Typescript の練習として type-challenges をやった備忘録です。
type-challenges のリポジトリ
前回 の続きをやっていきます。
Diff
Get an Object that is the difference between O & O1
問題
解答例
| Exclude < keyof O , keyof O1 >
| Exclude < keyof O1 , keyof O >]: P extends keyof O
お互いのオブジェクト型で重複していないプロパティのみ取り出す問題です。
単純に keyof O,keyof O1 両方から割り当て可能な型を除き、それを Mapped Types でオブジェクト型にまとめます。
使用している機能の詳細は下のリンクを参照してください。
Keyof Type Operator
Mapped Types
Indexed Access Types
Conditional Types
Exclude(Utility Types)
AnyOf
Implement Python liked any function in the type system. A type takes the Array and returns true if any element of the Array is true. If the Array is empty, return false.
問題
解答例
type Falsy = [ 0 , "" , false , [], Record < any , never >, null , undefined ][ number ];
type AnyOf < T extends readonly any []> = T extends [infer L , ...infer R ]
T の中に javascript の Falsy のような型が存在するかどうか判定する問題です。
Falsy(MDN)
最初に思いついたのは下のような形でしたが、予想とは異なり false が返ってきていました。
type Falsy = [ 0 , "" , false , [], {}, null , undefined ][ number ];
type AnyOf < T extends readonly any []> = T extends [infer L , ...infer R ]
type A = AnyOf <[ 1 , "test" , true , [ 1 ], { name : "test" }, { 1 : "test" }]>; // false
調べたところ、{}がかなり特殊な方のようです。
type IsEmpty < T > = T extends {} ? true : false ;
type D = IsEmpty < undefined >;
type F = IsEmpty < number >;
type G = IsEmpty < string >;
type I = IsEmpty <[ null ]>;
type J = IsEmpty <[ number ]>;
type L = IsEmpty <{ aaa : string }>;
type M = IsEmpty <{ aaa : null }>;
そこで {} の代わりに Record<any,never>を使って空オブジェクトを表現します。
type IsEmpty < T > = T extends Record < any , never > ? true : false ;
type D = IsEmpty < undefined >;
type F = IsEmpty < number >;
type G = IsEmpty < string >;
type I = IsEmpty <[ null ]>;
type J = IsEmpty <[ number ]>;
type L = IsEmpty <{ aaa : string }>;
type M = IsEmpty <{ aaa : null }>;
uhyo 様が記事を書かれていました
TypeScript の型入門
IsNever
Implement a type IsNever, which takes input type T. If the type of resolves to never, return true, otherwise false.
問題
解答例
type IsNever < T > = [ T ] extends [ never ] ? true : false ;
T が never かどうか判定する問題です。そのまま書くと下のようにエラーがでてしまいました。
type IsNever < T > = T extends never ? true : false ;
type A = IsNever < never >; // never
T extends …の T が never だと何があっても never に分類されるようです。
Distributive Conditional Types
T が never の時の、T extends .. は、問答無用で never になる
なので、[T]として、never ではなく[never]として判定させるようにすれば、エラーを解決できました。
IsUnion
Implement a type IsUnion, which takes an input type T and returns whether T resolves to a union type.
問題
解答例
type IsUnion < T , U = T > = [ T ] extends [ never ]
T がユニオン型かどうか判定する問題です。上で少しでてきた Distributive Conditional Types を活用します。
はじめに下のようにただ分配するだけの型を作ります。
type IsUnion < T > = T extends never ? never : T [];
type A = IsUnion < string | number >; // string[] | number[]
すると T がユニオン型の場合分配されるので、分配される前の T[]と比較すれば判定できそうです。
type IsUnion < T , U = T > = T extends never
type A = IsUnion < string | number >;
// = (string | number)[] extends string[] ? false : true
// |(string | number)[] extends number[] ? false : true
type B = IsUnion < never >; // never
最後に never が与えられた場合に、false を返すように上の IsNever をベースに条件分岐を追加します。
type IsUnion < T , U = T > = [ T ] extends [ never ]
ReplaceKeys
Implement a type ReplaceKeys, that replace keys in union types, if some type has not this key, just skip replacing, A type takes three arguments.
問題
解答例
type ReplaceKeys < U , T , Y extends Record < any , unknown >> = U extends never
[ P in keyof U ]: P extends T ? ( P extends keyof Y ? Y [ P ] : never ) : U [ P ];
はじめに、U が一旦ユニオン型ではないとして考えます。テストケースを見ると次のような処理を組めば良さそうです。
U のプロパティから T に割り当て可能なものを探す
T が Y のプロパティに存在すれば Y のプロパティ型、存在しなければ never 型に置き換える
やりたいことはわかったのでこれを実装します。おそらく、下のような形になるでしょうか。
type ReplaceKeys < U , T , Y extends Record < any , unknown >> = {
[ P in keyof U ]: P extends T ? ( P extends keyof Y ? Y [ P ] : never ) : U [ P ];
最後にこれをユニオン型でも出来ようできるように、T を Conditional Types で分解します。
type ReplaceKeys < U , T , Y extends Record < any , unknown >> = U extends never
[ P in keyof U ]: P extends T ? ( P extends keyof Y ? Y [ P ] : never ) : U [ P ];
Remove Index Signature
Implement RemoveIndexSignature<T> , exclude the index signature from object types.
問題
解答例
type RemoveIndexSignature < T > = {
[ P in keyof T as string extends P
どう考えても keyof を使う問題ですが、Index Signatures に keyof を使ったときの動作がわからなかったのでとりあえず調べてみます。
Index Signatures
Keyof Type Operator
英語ができないので、雰囲気で読んでみると Index Signatures に number が設定されている場合 keyof は number を、
string が設定されている場合は string | number を返すようです。
type Arrayish = { [ n : number ]: unknown };
type A = keyof Arrayish ; // number
type Mapish = { [ k : string ]: boolean };
type M = keyof Mapish ; // string | number
Note that in this example, M is string | number — this is because JavaScript object keys are always coerced to a string, so obj[0] is always the same as obj[“0”].
しかし、symbol が設定されている場合の説明がないので、一回試してみます。結果が下です。
type Check < T > = { [ P in keyof T ]: P };
const foobar = Symbol ( "foobar" );
// [foobar]: typeof foobar;
string が設定されているときに string | number が返ってきていない気がしますが、ひとまず symbol の場合は symbol が返ってくるようです。
ここまでの検証をまとめると Index Signatures に keyof を使うと string | number | symbol のどれかが返ってくることがわかりました。
ということで、とりあえず Index Signatures が never になるような型を作ってみます。
type RemoveIndexSignature < T > = {
[ P in keyof T ]: string extends P
type B = RemoveIndexSignature < Bar >;
type C = RemoveIndexSignature < FooBar >;
ここで、Mapped Types で使用できる as を使います。as を使うことで {[P in T]:…}としていた P に対して変更を加えられます。
Key Remapping via as
type RemoveIndexSignature < T > = {
[ P in keyof T as string extends P
as を使わなくても keyof T のうち T[P]が never なものを取り除き、それを使って T を再構成すれば行けると思いますが未検証です。
Percentage Parser
Implement PercentageParser. According to the /^(+|-)?(\d*)?(%)?$/ regularity to match T and get three matches.
The structure should be: [plus or minus, number, unit] If it is not captured, the default is an empty string.
問題
解答例
type ParseSign < A extends string > = A extends ` ${ "+" | "-" }${ string } `
? A extends ` ${ infer L }${ string } `
type ParsePercentage < A extends string > = A extends ` ${ string } %` ? "%" : "" ;
S extends string = ParseSign < A >,
P extends string = ParsePercentage < A >,
> = A extends ` ${ S }${ infer N }${ P } ` ? [ S , N , P ] : never ;
この問題は3つの工程に分解して考えます。
符号がついているか判定。ついていたらその符号を返す
%がついているか判定。ついていれば%を返す
数字部分を取り出す
まず、1 と 2 の判定部分を作ります。これはサクッと作れるでしょう。
type ParseSign < A extends string > = A extends ` ${ "+" | "-" }${ string } `
? A extends ` ${ infer L }${ string } `
type ParsePercentage < A extends string > = A extends ` ${ string } %` ? "%" : "" ;
次にこれらを組み合わせて、3 の数字取り出しを実現します。
S extends string = ParseSign < A >,
P extends string = ParsePercentage < A >,
> = A extends ` ${ S }${ infer N }${ P } ` ? [ S , N , P ] : never ;
type A = PercentageParser < "+100%" >;
Drop Char
Drop a specified char from a string.
問題
解答例
: S extends ` ${ infer L }${ infer R } `
: DropChar < R , C , ` ${ V }${ L } ` >
本当は C が 1 文字かどうかの検証を入れるべきだと思いますが、Drop Char なので見なかったことにします。
終わりに
今日は時間がとれなかったのに加えて、問題自体の難易度も上がり昨日と比較して 3 分の 1 しか解くことができませんでした(今日 8 問、昨日 24 問)。
問題の難易度が上がったぶん、自分の Typesript 筋が強くなっていっているとありがたいですが…
もし、記事内容に誤り等があれば Issue をだしていただくか、私の DM にお願いします。
残りの問題は明日以降解きます。