type challenges【上級編】
Simple Vue
何も思いつかないくらい難しかった。
分かりやすい解説があった
答えは↓のようになる
// computedの型が関数のためそのままアクセスすると関数型が帰ってきてしまうので、関数の返り値の型が返るようにする
type GetComputed<C> = C extends Record<string, (...args: any[]) => any>
? { [key in keyof C]: ReturnType<C[key]> }
: never
declare function SimpleVue<D, C, M>(options: {
data: (this: {}) => D, // thisにアクセスできないようにする
computed: C & ThisType<D & C>,
methods: M & ThisType<D & GetComputed<C> & M>,
}): any
またいつかチャレンジしたい
Currying 1
カリー化する
カリー化とは、複数の引数を取る関数を、それぞれの 1 つの引数を取る関数の列に変換するテクニックです。
解答
関数の引数と返り値をinferで推定して、そいつをもとにカリー化する_currying型を作成した。
引数が空の時に() => ReturnValue
をすぐに返す必要がある。
type _currying<Args extends unknown[], Ret> = Args extends [infer Head, ...infer Rest]
? (arg0: Head) => _currying<Rest, Ret>
: Ret
declare function Currying<T>(fn: T): T extends (...args: infer U) => infer V
? U extends []
? () => V
: _currying<U, V>
: never
Union To Intersection
UnionからIntersectionに変換
type I = Union2Intersection<'foo' | 42 | true> // expected to be 'foo' & 42 & true
自分の解答(不正解)
unionだからdistributionを使うんだろうなと思って↓のコードを書いてみたがダメだった。
type UnionToIntersection<U, T = U> = U extends U ? [T] extends [U] ? U : U & UnionToIntersection<Exclude<T, U>> : never
これだとUnionToIntersection<'a' | 'b'> → 'a' & 'b' | 'b' & 'a' → never
になる
解答
type UnionToIntersection<U> = (U extends infer R ? (x: R) => any : never) extends (x: infer V) => any ? V : never
解説
-
(U extends infer R ? (x: R) => any : never)
でunion distributionを発生
- 例えば
'a' | 'b'
の場合(x: 'a') => any | (x: 'b') => any
が生成される
-
(x: R) => any extends (x: infer V) => any
で引数の型を推論
- ここで引数推論する引数は
(x: 'a') => any | (x: 'b') => any
の引数。 -
(x: 'a') => any | (x: 'b') => any
はaも受け取れてbも受け取ることができる関数。つまり(x: 'a' & 'b') => any
と同義。 - そのため最終的に
(x: 'a') => any | (x: 'b') => any
の引数の型の推論結果は'a' & 'b'になる。 - 以上でintersectionがとれる
参考
Get Required
optional propertyを除外
解答
Propertyを必須にした時との差を見て、差分があったらoptional, なかったらoptionalではないと判定
(判定の際はRequiredを使う。自前実装で-?をやった時はダメだった。)
type GetRequired<T> = { [k in keyof T as T[k] extends Required<T>[k] ? k : never]: T[k] };
Get Optional
今度はoptionol propetyのみを抽出
解答
GetRequiredの反対をすれば良い
type GetOptional<T> = { [k in keyof T as T[k] extends Required<T>[k] ? never : k]: T[k] };
Required Keys
オブジェクトから必須プロパティのキーをunionで返す。
解答
Get Requiredと同じようにオブジェクトからRequiredプロパティだけのオブジェクトを取得して、そのキーを取得
type RequiredKeys<T> = keyof {
[k in keyof T as T[k] extends Required<T>[k] ? k : never]: any
}
Optional Keys
オブジェクトから任意プロパティのキーをunionで返す。
解答
type OptionalKeys<T> = keyof {
[k in keyof T as T[k] extends Required<T>[k] ? never: k]: any
}
Capitalize Words
各文字の先頭を大文字に変換
解答
まず↓のコードを書いてみた
type EndOfWords = ' ' | '.' | ','
type CapitalizeWords<S extends string> = S extends `${infer Head}${EndOfWords}${infer Rest}`
? `${Capitalize<Head>} ${CapitalizeWords<Rest>}`
: Capitalize<S>
これだとhello world,hoge
のように、文字列中に区切り文字が複数ある場合にどちらも区切り方の組み合わせの数だけdistributionが発生してしまう。
なので区切り文字をunionで管理するのではなく、リストで管理するようにして、区切り文字1文字ずつ処理を行うようにした。↓
type EndOfWordsList = [' ', '.', ',', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '+', '{', '}', '|', '🤣']
type CapitalizeWords<S extends string, WordsList extends string[] = EndOfWordsList> = WordsList extends [infer Word extends string, ...infer RestWords extends string[]]
? S extends `${infer Head}${Word}${infer Rest}`
? `${Capitalize<Head>}${Word}${CapitalizeWords<Rest, WordsList>}`
: CapitalizeWords<S, RestWords>
: Capitalize<S>
C-printf Parser
C言語のprintf関数likeな型を実装する。
条件として↓の型が与えられる。
type ControlsMap = {
c: 'char'
s: 'string'
d: 'dec'
o: 'oct'
h: 'hex'
f: 'float'
p: 'pointer'
}
この型を用いて文字列中のフォーマットプレースホルダを抽出する。
例えば"The result is %d.",
という文字列が与えられた場合は['dec']
を返す。
解答
${infer Head}%${infer Next}${infer Tail}
で%の次の文字をparseすることができた。
parseした文字がControlsMapに入っているか確認して、入っていたらmapの値を取り出す。
type ParsePrintFormat<S extends string, Ret extends string[] = []> = S extends `${infer Head}%${infer Next}${infer Tail}`
? Next extends keyof ControlsMap
? ParsePrintFormat<`${Head}${Tail}`, [...Ret, ControlsMap[Next]]>
: ParsePrintFormat<`${Head}${Tail}`, Ret>
: Ret
Vue Basic Props
Vueのpropsを定義する
解答
↓のようにPropsを変換する型を定義したところクラスインスタンスだけ通らず、直し方が分からなかったのでgive upした。
type ConvertProps<P extends object> = {
[k in keyof P]: P[k] extends { type: infer T }
? T extends any[]
? T[number] extends (...args: any) => any
? ReturnType<T[number]>
: never
: T extends (...args: any) => any
? ReturnType<T>
: T
: P[k] extends (...args: any) => any
? ReturnType<P[k]>
: any
}
type GetComputed<C> = C extends Record<string, (...args: any[]) => any>
? { [k in keyof C]: ReturnType<C[k]> }
: never
declare function VueBasicProps<P extends object, D, C, M>(options: {
props: P,
data: (this: ConvertProps<P>) => D,
computed: C & ThisType<D & C>,
methods: M & ThisType<ConvertProps<P> & D & GetComputed<C> & M>,
}): any
正解
instanceのtypeを取得するInstanceTypeという型があるのでそれを使うとtypeof ClassAにならずに済む
type ConvertProps<P extends object> = {
[k in keyof P]: P[k] extends { type: infer T }
? T extends any[]
? T[number] extends (...args: any) => any
? ReturnType<T[number]>
: never
: T extends new (...args: any) => any
? T extends typeof String | typeof Boolean | typeof Number
? ReturnType<T>
: InstanceType<T>
: T
: P[k] extends (...args: any) => any
? ReturnType<P[k]>
: any
}
type GetComputed<C> = C extends Record<string, (...args: any[]) => any>
? { [k in keyof C]: ReturnType<C[k]> }
: never
declare function VueBasicProps<P extends object, D, C, M>(options: {
props: P,
data: (this: ConvertProps<P>) => D,
computed: C & ThisType<D & C>,
methods: M & ThisType<ConvertProps<P> & D & GetComputed<C> & M>,
}): any
IsAny
any型を判定する型
自分の解答
もちろん extends anyでは判定できなかった。やり方思い浮かばずgive up
正解
type IsAny<T> = 0 extends 1 & T ? true : false
解説
0 extends 1 & T
は普通false。だが1 & any
がany
になるため0 extends any
でtrueになる。
参照: https://stackoverflow.com/questions/49927523/disallow-call-with-any/49928360#49928360
Typed Get
lodashのgetを実装
type Data = {
foo: {
bar: {
value: 'foobar';
count: 6;
};
included: true;
};
hello: 'world';
};
type A = Get<Data, 'hello'>; // 'world'
type B = Get<Data, 'foo.bar.count'>; // 6
type C = Get<Data, 'foo.bar'>; // { value: 'foobar', count: 6 }
解答
${infer Head}.${infer Rest}
で.
前を抜き出し、それがkeyof T
をextendsしていたらTから値を取り出す という操作を繰り返すことで取得できた。
type Get<T extends object, K extends string> = K extends keyof T
? T[K]
: K extends `${infer Head}.${infer Rest}`
? Head extends keyof T
? T[Head] extends object
? Get<T[Head], Rest>
: never
: never
: never
String to Number
文字列から数値に変換
解答
次のような手順による変換を考えた
- 文字列から1文字目を取り出す
- その文字を数値に変換(文字と数値のMapを用いる)
- 数値のままだと扱いづらいので、数値をその数値分の長さを持った配列に変換して保持しておく
- 2文字目を取り出して同じように数値に変換→配列に変換
- 前の文字から生成された配列は一つ上の位のものなので、配列長を10倍して今の文字から生成した配列と結合
- この操作を文字列全て見終わるまで続け、最後に最終的な配列長を返す
上記手順を実装したところ通った。ただし再帰の上限により9999までしか変換できない。
type NumberMap = {
'0': 0,
'1': 1,
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
}
type ToList<N extends number, Ret extends any[] = []> = Ret['length'] extends N
? Ret
: ToList<N, [...Ret, any]>
type TenTimes<T extends any[], Count extends any[] = [], Ret extends any[] = []> = Count['length'] extends 10
? Ret
: TenTimes<T, [...Count, any], [...Ret, ...T]>
type ToNumber<S extends string, Ret extends any[] = []> = S extends `${infer Head}${infer Rest}`
? Head extends keyof NumberMap
? ToNumber<Rest, [...TenTimes<Ret>, ...ToList<NumberMap[Head]>]>
: never
: Ret['length']
他の人の解答
すごいと思った解答が↓
type ToNumber<S extends string, T extends any[] = []> = S extends `${T["length"]}`
? T["length"]
: ToNumber<S, [...T, any]>
ものすごくシンプル。
ただ再帰の上限で44までしか計算できないらしい。
Tuple Filter
Tupleから指定された型をフィルタリング
解答
除外する型にunionが与えられた場合にうまく対処できず不正解だった。union distributionを無駄にゴニョゴニョやってしまった。
type IsNever<T> = T[] extends never[] ? true : false
type _EqualSome<T, U> = IsNever<U> extends false
? U extends U
? Equal<T, U>
: never
: Equal<T, U>
type EqualSome<T, U, E = _EqualSome<T, U>> = Equal<E, boolean> extends true
? true
: E
type q = EqualSome<undefined, undefined | null>
type w = EqualSome<never, never | null | undefined> // falseになってしまう
type t = _EqualSome<never, never | null | undefined> // falseになってしまう
type r = EqualSome<null, never | null | undefined>
type FilterOut<T extends any[], F, Ret extends any[] = []> = T extends [infer Head, ...infer Rest]
? EqualSome<Head, F> extends true
? FilterOut<Rest, F, Ret>
: FilterOut<Rest, F, [...Ret, Head]>
: Ret
正解
Equalとか使わずに普通にextendsでよかったらしい。ただしdistributionが発生しないようにするために[]で囲む。
type FilterOut<T extends any[], F>
= T extends [infer R, ...infer Rest]
? [R] extends [F]
? FilterOut<Rest, F>
: [R, ...FilterOut<Rest, F>]
: []
Tuple to Enum Object
TupleからEnum likeなobjectを生成
解答
TupleにindexをつけるためのKeyWithIndexという型を作成した。
indexを付与する場合にはこの型を用いることでいけた。
type AddIndexToList<T extends readonly string[], Ret extends [string, number][] = []> = T extends readonly [...infer Rest extends string[], infer Tail extends string]
? AddIndexToList<Rest, [[Tail, Rest['length']], ...Ret]>
: Ret
type KeyWithIndex<T extends readonly string[], U extends [string, number][] = AddIndexToList<T>> = {
[k in U[number] as k[0]]: k[1]
}
type Enum<T extends readonly string[], N extends boolean = false, U extends object = KeyWithIndex<T>> = {
readonly [k in T[number] as Capitalize<k>]: N extends true ? k extends keyof U ? U[k] : never : k
}
printf
Expect<Equal<Format<'abc'>, string>>,
Expect<Equal<Format<'a%sbc'>, (s1: string) => string>>,
Expect<Equal<Format<'a%dbc'>, (d1: number) => string>>,
Expect<Equal<Format<'a%%dbc'>, string>>,
Expect<Equal<Format<'a%%%dbc'>, (d1: number) => string>>,
Expect<Equal<Format<'a%dbc%s'>, (d1: number) => (s1: string) => string>>,
こんな感じに変換する型を作成
解答
割とシンプルに%の次の文字がsかdだったらstringかnumberを引数に持たせて、その次以降の文字を再帰的に処理した結果を関数の返り値にすればいけた
type Format<T extends string> = T extends `${infer prefix}%${infer C}${infer suffix}`
? C extends 's'
? (s1: string) => Format<suffix>
: C extends 'd'
? (d1: number) => Format<suffix>
: Format<suffix>
: string
Length of string 2
文字列の長さを取得。再帰上限に引っかからないように工夫をする必要があるらしい。
解答
type LengthOfString<S extends string, A extends any[] = []> = S extends `${any}${infer Rest}`
? LengthOfString<Rest, [...A, any]>
: A['length']
特に工夫せずともテストケースは通ってしまった。
工夫するとしたら↓のように10文字を1回の再帰で取得か
type LengthOfString<S extends string, A extends any[] = []> = S extends `${any}${any}${any}${any}${any}${any}${any}${any}${any}${any}${infer Rest}`
? LengthOfString<Rest, [...A, any, any, any, any, any, any, any, any, any, any]>
: S extends `${any}${infer Rest}`
? LengthOfString<Rest, [...A, any]>
: A['length']
Union To Tuple
UnionからTupleに変換
解答
難しかった、、、思いつかなかった
解説
Union to intersectionと似たような考えで行けるらしい。
https://github.com/type-challenges/type-challenges/issues/737 の解答に詳しい解説が載っていた。
// https://github.com/type-challenges/type-challenges/issues/737
/**
* UnionToIntersection<{ foo: string } | { bar: string }> =
* { foo: string } & { bar: string }.
*/
type UnionToIntersection<U> = (
U extends unknown ? (arg: U) => 0 : never
) extends (arg: infer I) => 0
? I
: never;
/**
* LastInUnion<1 | 2> = 2.
*/
type LastInUnion<U> = UnionToIntersection<
U extends unknown ? (x: U) => 0 : never
> extends (x: infer L) => 0
? L
: never;
/**
* UnionToTuple<1 | 2> = [1, 2].
*/
type UnionToTuple<U, Last = LastInUnion<U>> = [U] extends [never]
? []
: [...UnionToTuple<Exclude<U, Last>>, Last];
- UnionをUnionを引数として持った関数のintersectionに変換
- 1.の後、関数型を用いて関数のintersectionから最後の要素だけを取る(これでunionから最後の要素を取得することが可能)
- 1,2の操作を繰り返して1つずつ要素を取得する
詳しくはgithubの解説参照
String join
join関数を実装
解答
type Join<D extends string, P extends string[], Ret extends string = ""> = P extends [infer Head extends string, ...infer Rest extends string[]]
? Ret extends ""
? Join<D, Rest, Head>
: Join<D, Rest, `${Ret}${D}${Head}`>
: Ret
declare function join<D extends string>(delimiter: D): <P extends string[]>(...parts: P) => Join<D, P>
初め
declare function join<D extends string, P extends string[]>(delimiter: D): (...parts: P) => Join<D, P>
と書いていて、Pの値が取得できなかった。
書く場所を変えたら通った。
Deep Pick
↓のように再帰的にpickを行う
type obj = {
name: 'hoge',
age: 20,
friend: {
name: 'fuga',
age: 30,
family: {
name: 'baz',
age: 1
}
}
}
type T1 = DeepPick<obj, 'name'> // { name : 'hoge' }
type T2 = DeepPick<obj, 'name' | 'friend.name'> // { name : 'hoge' } & { friend: { name: 'fuga' }}
type T3 = DeepPick<obj, 'name' | 'friend.name' | 'friend.family.name'> // { name : 'hoge' } & { friend: { name: 'fuga' }} & { friend: { family: { name: 'baz' }}}
解答
Keyを.
区切りでparseしていき、.
前の文字列が現在見ているobjectに含まれていたらそれを取得し、再帰的に_DeepPickを繰り返す。
Keyにはunionが与えられ、最終的な解答はintersectionが求められているので以前やったUinonToIntersectoinでunionからintersectionに変換する。
という方針で実装したらいけた
type UnionToIntersection<U> = (U extends infer R ? (x: R) => any : never) extends (x: infer V) => any ? V : never
type _DeepPick<T extends object, K extends string> = K extends `${infer Head}.${infer Rest}`
? Head extends keyof T
? T[Head] extends object
? {[k in Head] : DeepPick<T[Head], Rest>}
: never
: never
: K extends keyof T
? Pick<T, K>
: never
type DeepPick<T extends object, K extends string> = UnionToIntersection<_DeepPick<T, K>>
Pinia
Piniapという状態管理ライブラリの型を実装する
SimpleVueと似たような感じ
解答
gettersは定義上は関数だが外から見ると関数ではなく変数に見えるようにするのがポイント
これを実現するためにGetObjectReturnTypeという型を定義した。
あとはテストケース通りstate, getters, actionsの型を定義して、storeの型も定義してやればいける。
getterはReadonlyなことにも注意
type GetObjectReturnType<T> = { [key in keyof T as key extends `${infer k}`? k : never]: T[key] extends (...args: any) => any ? ReturnType<T[key]> : never}
declare function defineStore<S, G, A>(store: {
id: string;
state: () => S;
getters: G & ThisType<GetObjectReturnType<G> & Readonly<S>>;
actions: A & ThisType<S & Readonly<G> & A>
}): S & Readonly<GetObjectReturnType<G>> & A
Camelize
各プロパティ名をキャメルケースに変換。ネストしているプロパティも変換する。
解答
snake_caseからCamelCaseに変換する型は以前やったので持ってきた。
配列の場合は各配列の要素ごとにCamelCaseにする必要があったので、それを行うCamelizeArrayという型を作成した。あとは再帰的にそれらを呼んでやるだけ。
type CamelCase<S extends string, isHead extends boolean = true> = S extends `${infer Head}${infer Rest}`
? Rest extends `_${infer _Rest}`
? `${Head}${CamelCase<Capitalize<_Rest>, false>}`
: isHead extends true
? `${Lowercase<Head>}${CamelCase<Uncapitalize<Rest>, false>}`
: `${Head}${CamelCase<Uncapitalize<Rest>, false>}`
: S
type CamelizeArray<T extends object[]> = T extends [infer Head extends object, ...infer Rest extends object[]]
? [Camelize<Head>, ...CamelizeArray<Rest>]
: []
type Camelize<T extends object> = T extends object[]
? CamelizeArray<T>
: {
[k in keyof T as k extends string ? CamelCase<k> : never]: T[k] extends object ? Camelize<T[k]> : T[k]
}
Drop String
文字列から、指定された文字を削除する
type Butterfly = DropString<'foobar!', 'fb'> // 'ooar!'
解答
難しくなかった。
除外する文字列をそのまま扱うのが大変なので、unionに変換し、そのunionを各文字がextendしているかどうかを判定するだけでいけた。
type StringToUnion<T extends string, Ret extends string[] = []> = T extends `${infer Head}${infer Rest}`
? StringToUnion<Rest, [...Ret, Head]>
: Ret[number]
type DropString<S extends string, R extends string, Ret extends string = ""> = S extends `${infer Head}${infer Rest}`
? Head extends StringToUnion<R>
? DropString<Rest, R, Ret>
: DropString<Rest, R, `${Ret}${Head}`>
: Ret
Split
文字列を指定された文字で分割するsplit関数の型を実装
解答
基本的な部分はすぐ実装できたが細かい例外ケースが面倒だった。
// 例外ケース
Expect<Equal<Split<'Hi! How are you?', ''>, ['H', 'i', '!', ' ', 'H', 'o', 'w', ' ', 'a', 'r', 'e', ' ', 'y', 'o', 'u', '?']>>,
Expect<Equal<Split<'', ''>, []>>,
Expect<Equal<Split<'', 'z'>, ['']>>,
Expect<Equal<Split<string, 'whatever'>, string[]>>,
type Split<S extends string, SEP extends string, Ret extends string[] = []> = S extends `${infer H extends string}${SEP}${infer R extends string}`
? Split<R, SEP, [...Ret, H]>
: S extends ""
? Ret extends []
? S extends SEP
? Ret
: [S]
: Ret
: Equal<S, string> extends true
? S[]
: [...Ret, S]
他の人の解答見たらもう少し綺麗に条件分岐していた
IsRequiredKey
↓のようにkeyがrequiredかどうかを判定
IsRequiredKey<{ a: number; b?: string }, 'a'>
解答
undefinedがT[K]をextendsしているかどうかでoptionalかどうか判定できるのでそれを使う。
複数キーを指定する場合でもtrue | false => false, true | true => trueのようにunionが最終的にマージされる
type IsRequiredKey<T, K extends keyof T> = undefined extends T[K] ? false : true
他の人の解答
NonNullableを使うとよさそう
type IsRequiredKey<T, K extends keyof T> = T[K] extends NonNullable<T[K]> ? true : false
Object From Entries
↓のようにプロパティ名と型を持ったTupleのUnionからオブジェクトを生成
interface Model {
name: string;
age: number;
locations: string[] | null;
}
type ModelEntries = ['name', string] | ['age', number] | ['locations', string[] | null];
type result = ObjectFromEntries<ModelEntries> // expected to be Model
解答
tupleのunionが与えられるのでそれをうまく扱うのがポイント
type UnionToIntersection<U> = (U extends infer R ? (x: R) => any : never) extends (x: infer V) => any ? V : never
type _UnionFromEntries<T extends [string, any]> = T extends T
? {
[k in T[0]]: T[1]
} : never
type ObjectFromEntries<T extends [string, any]> = UnionToIntersection<_UnionFromEntries<T>>
union distributionを発生させて各プロパティごとのオブジェクトのunionを生成する_UnionFromEntries
を作成して、その後UnionToIntersection
でunionからintersectionに変換するようにした。
この時点で型的には等しかったが、Equal関数が通らなかったので、もう1アレンジした↓
type UnionToIntersection<U> = (U extends infer R ? (x: R) => any : never) extends (x: infer V) => any ? V : never
type _UnionFromEntries<T extends [string, any]> = T extends T
? {
[k in T[0]]: T[1]
} : never
type _ObjectFromEntries<T extends [string, any]> = UnionToIntersection<_UnionFromEntries<T>>
type ObjectFromEntries<T extends [string, any], U = _ObjectFromEntries<T>> = {
[k in T[0]]: k extends keyof U ? U[k] : never
}
これで通った
他の人の解答
他の人の解答見たらもっとエレガントなものがあった
type ObjectFromEntries<T extends [string, any]> = {
[K in T[0]]: T extends [ K, any ] ? T[1] : never
}
type ObjectFromEntries<T extends [string,any]> = {
[K in T as K[0]]:K[1]
}
IsPalindrome
回文かどうかを判定
解答
type IsPalindrome<T extends string | number> = `${T}` extends `${infer Head}${infer Rest}`
? Rest extends `${infer Center}${Head}`
? IsPalindrome<Center>
: Rest extends ""
? true
: false
: true
先頭の文字をとって、最後の文字が先頭の文字と一致しているかを判定し一致していたら再帰的にIsPalindromeを実行する
最初`${T}` extends `${infer Head}${infer Center}${infer Tail}`
で1発で先頭と最後の文字を取れるかと思ったが流石に取れなかった。
MutableKeys
mutableなpropertyのkeyを取得
解答
readonlyかどうかをEqual<Pick<T, k>, Readonly<Pick<T, k>>>
で判定
type MutableKeys<T> = keyof {
[k in keyof T as Equal<Pick<T, k>, Readonly<Pick<T, k>>> extends true ? never : k]: T[k]
}
Intersection
配列の各要素から共通の要素を取得
type Res = Intersection<[[1, 2], [2, 3], [2, 2]]>; // expected to be 2
type Res1 = Intersection<[[1, 2, 3], [2, 3, 4], [2, 2, 3]]>; // expected to be 2 | 3
type Res2 = Intersection<[[1, 2], [3, 4], [5, 6]]>; // expected to be never
type Res3 = Intersection<[[1, 2, 3], [2, 3, 4], 3]>; // expected to be 3
type Res4 = Intersection<[[1, 2, 3], 2 | 3 | 4, 2 | 3]>; // expected to be 2 | 3
type Res5 = Intersection<[[1, 2, 3], 2, 3]>; // expected to be never
解答
type InitialValue<T extends unknown[]> = T[0] extends number[]
? T[0][number]
: T[0]
type Intersection<T extends unknown[], Ret = InitialValue<T>> = T extends [infer Head, ...infer Rest extends unknown[]]
? Head extends number[]
? Intersection<Rest, Extract<Ret, Head[number]>>
: Intersection<Rest, Extract<Ret, Head>>
: Ret
Extract<1 | 2, 2 | 3> // 2
のようにUnionから共通の要素を取得できるのでそれを使ってリストの各要素から共通要素をとっていく
他の人の解答
もっと短い回答があった
type Intersection<T> =
T extends [infer First, ...infer Rest] ? (
(First extends unknown[] ? First[number] : First) & Intersection<Rest>
) : unknown
Binary to Decimal
二進数から十進数に変換
回答
数値を保存する用の配列を持っておく。
先頭の数字から見ていく。まず配列の長さを2倍にして、現在見ている数字が1だったらその配列に要素を足して、0だったら何もしないという操作を繰り返す。
type BinaryToDecimal<S extends string, Ret extends unknown[] = []> = S extends `${infer Head}${infer Rest}`
? Head extends '1'
? BinaryToDecimal<Rest, [...Ret, ...Ret, any]>
: BinaryToDecimal<Rest, [...Ret, ...Ret]>
: Ret['length']
Object Key Paths
オブジェクトの各プロパティの名前をユニオンで繋げる。
その際に子孫プロパティだったらparent.child
のようにする。また配列の場合はparent.0 | parent[0] | parent.[0]
のようにする
回答
配列の扱いが面倒だった。keyof [hoge, fuga]
で0, 1が取れそうだったのでそれを使ってkeyがnumberの場合は[]
をつけたりするようにしたらいけた。
type KeyWithPrefix<T extends object, Prefix extends string = "", K extends keyof T = keyof T> = Prefix extends ""
? K
: K extends string | number
? `${Prefix}.${K}` | (K extends number ? `${Prefix}${"." | ""}[${K}]` : never)
: never
type ObjectKeyPaths<T extends object, Prefix extends string = "", K extends keyof T = keyof T> = K extends K
? T[K] extends object
? K extends string | number
? Prefix extends ""
? KeyWithPrefix<T, Prefix> | ObjectKeyPaths<T[K], `${K}`>
: KeyWithPrefix<T, Prefix> | ObjectKeyPaths<T[K], `${Prefix}.${K}`>
: never
: KeyWithPrefix<T, Prefix>
: never
Two Sum
第一引数に数値の配列、第二引数に数値を渡して第一引数の配列内の2つの数値を足した数が第二引数の数値に一致する場合があるかどうかを判定
type sum1 = TwoSum<[3, 2, 4], 6> // true
type sum2 = TwoSum<[2, 7, 11, 15], 15> // false
回答
配列内の全てのペアの組み合わせを取得するAllPairという型を作成し、それを1つずつ足していき一致する場合があるかを判定したらいけた。無駄に長い...
type _AllPair<T extends number[], Ret extends number[] = []> = T extends [infer Head extends number, ...infer Rest extends number[]]
? _AllPair<Rest, [...Ret, Head]> | _AllPair<Rest, Ret>
: Ret
type AllPair<T extends number[], P = _AllPair<T>> = P extends [infer Head extends number, infer Second extends number, ...any]
? [Head, Second]
: never
type NumberToArray<T extends number, Arr extends any[] = []> = Arr['length'] extends T ? Arr : NumberToArray<T, [...Arr, any]>
type Plus<T extends number, U extends number> = [...NumberToArray<T>, ...NumberToArray<U>]['length']
type _TowSum<T extends number[], U extends number, P extends [number, number] = AllPair<T>> = P extends P
? Plus<P[0], P[1]> extends U
? true
: false
: never
type TwoSum<T extends number[], U extends number, V = _TowSum<T, U>> = Equal<V, boolean> extends true
? true
: V
他の人の解答見ても同じくらいの長さだったのでそんなに簡潔には書けないみたい。
Assign
Target objectに対してorigin object listのobjectのプロパティで上書きをしていく
type Target = {
a: 'a'
d: {
hi: 'hi'
}
}
type Origin1 = {
a: 'a1',
b: 'b'
}
type Origin2 = {
b: 'b2',
c: 'c'
}
type Answer = {
a: 'a1',
b: 'b2',
c: 'c'
d: {
hi: 'hi'
}
}
解答
listの要素を一つずつ見ていって、Target objectのプロパティとOrigin objectのプロパティが重複している場合はOrigin objectのプロパティで上書きをする。それ以外はそのままという操作を繰り返す。
Originに渡されたobjectがRecord<string, unknown>以外の場合は無視をしなければいけなかった。
type Assign<T extends Record<string, unknown>, U> =
U extends [infer Head, ...infer Rest]
? Head extends Record<string, unknown>
? Assign<{
[k in keyof T | keyof Head as k]: k extends keyof Head ? Head[k] : k extends keyof T ? T[k] : never
}, Rest>
: Assign<T, Rest>
: T
Maximum
配列から最大の値を取り出す
解答
2つの数値を比較して大きい方の数値を返すBiggerという型を作成して、配列の先頭から2つずつ取り出して数値の比較をして、大きい方を配列に入れてまた大小を比較という操作を繰り返すことで最大値を取得できた。
type Bigger<T extends number, U extends number, TA extends unknown[] = [], UA extends unknown[] = []> = TA['length'] extends T
? U
: UA['length'] extends U
? T
: Bigger<T, U, [...TA, unknown], [...UA, unknown]>
type Maximum<T extends number[], Ret = never> = T extends [infer Head extends number, infer Second extends number, ...infer Rest extends number[]]
? Maximum<[Bigger<Head, Second>, ...Rest], Bigger<Head, Second>>
: Ret
Capitalize Nest Object Keys
objectのkeyを再帰的にCapitalizeする
リストの場合はリスト内の各objectのkeyもCapitalizeする
解答
リストの各要素に対してCapitalizeするCapitalizeObjectArray
を定義して、与えられたobjectが配列だったらその型を使い、それ以外の場合は普通のCapitalizeをするようにしたらいけた
type CapitalizeObjectArray<T extends object[], Ret extends object[] = []> = T extends [infer Head, ...infer Rest extends object[]]
? CapitalizeObjectArray<Rest, [...Ret, CapitalizeNestObjectKeys<Head>]>
: Ret
type CapitalizeNestObjectKeys<T> = T extends object[] ?
CapitalizeObjectArray<T>
: {
[k in keyof T as k extends string ? Capitalize<k> : never]: T[k] extends object ? CapitalizeNestObjectKeys<T[k]> : T[k]
}
Replace Union
Equal<UnionReplace<Function | Date | object, [[Date, string], [Function, undefined]]>, undefined | string | object>
こんな感じでunionを与えられた置換配列に基づいて置き換える
解答
シンプルにunionの要素1つずつと、配列の各要素が一致するかどうかをみていって一致していたら置換、一致してなかったら配列の次の要素と一致しているかを確認という操作を繰り返したらいけた。
type UnionReplace<T, U extends [any, any][]> = T extends T
? U extends [[infer HeadFrom, infer HeadTo], ...infer Rest extends [any, any][]]
? Equal<T, HeadFrom> extends true
? HeadTo
: UnionReplace<T, Rest>
: T
: never
FizzBuzz
3の倍数でFizz, 5の倍数でBuzz, 15の倍数でFizzBuzzというやつ
解答
面倒だが地道にやる系のやつ。
まず数値を配列に変換する型を定義。
その型を使って3の倍数、5の倍数かどうかを判定する型をそれぞれ定義。
これを使って数字が与えられた時にnumber | Fizz | Buzz | FizzBuzzを返す型を定義。
type NumToArr<T, Ret extends unknown[] = []> = Ret['length'] extends T
? Ret
: NumToArr<T, [...Ret, unknown]>
type Div3<T, A extends unknown[] = NumToArr<T>> = A extends [unknown, unknown, unknown, ...infer Rest extends unknown[]]
? Div3<T, Rest>
: A extends []
? true
: false
type Div5<T, A extends unknown[] = NumToArr<T>> = A extends [unknown, unknown, unknown, unknown, unknown, ...infer Rest extends unknown[]]
? Div5<T, Rest>
: A extends []
? true
: false
type NumToFizzBuzz<N, D3 = Div3<N>, D5 = Div5<N>> = D3 extends true
? D5 extends true
? "FizzBuzz"
: "Fizz"
: D5 extends true
? "Buzz"
: N extends number
? `${N}`
: never
type FizzBuzz<N extends number, Ret extends string[] = []> = Ret['length'] extends N
? Ret
: FizzBuzz<N, [...Ret, NumToFizzBuzz<[...Ret, unknown]['length']>]>
Run-length encoding
AAABCCXXXXXXY
のような文字列が与えられた場合、エンコーディングされた文字列3AB2C6XY
を返す型を作成。また、デコーダーも作成。
解答
だいぶ長いがやっていることは単純。
エンコーダは今見ている文字と前の文字が一致していたら配列に保存して次の文字を見る。不一致だったらそこまで保存した配列を使ってエンコードする。
デコーダは数字と文字のセットごとにパースして、数字の分だけの長さを持った文字列を作成する型を作り、それを使ってセットごとに変換していく。
type EncodeFromArray<T extends string[]> = T extends []
? never
: T['length'] extends 1
? T[0]
: `${T['length']}${T[0]}`
type NumberMap = {
'2': 2,
'3': 3,
'4': 4,
'5': 5,
'6': 6,
'7': 7,
'8': 8,
'9': 9,
}
type Number = keyof NumberMap
type ArrToStr<Arr extends string[], Ret extends string = ""> = Arr extends [infer Head extends string, ...infer Rest extends string[]]
? ArrToStr<Rest, `${Ret}${Head}`>
: Ret
type DecodeFromStr<NumStr extends Number, Str extends string, Arr extends string[] = []> = Arr['length'] extends NumberMap[NumStr]
? ArrToStr<Arr>
: DecodeFromStr<NumStr, Str, [...Arr, Str]>
namespace RLE {
export type Encode<S extends string, Arr extends string[] = [], Ret extends string = ""> = S extends `${infer Head}${infer Rest}`
? Arr extends []
? Encode<Rest, [Head], Ret>
: Head extends Arr[0]
? Encode<Rest, [...Arr, Head], Ret>
: Encode<Rest, [Head], `${Ret}${EncodeFromArray<Arr>}`>
: Arr extends []
? Ret
: `${Ret}${EncodeFromArray<Arr>}`
export type Decode<S extends string, Ret extends string = ""> = S extends `${infer N extends Number}${infer Str}${infer Rest}`
? Decode<Rest, `${Ret}${DecodeFromStr<N, Str>}`>
: S extends `${infer Head}${infer Rest}`
? Decode<Rest, `${Ret}${Head}`>
: Ret
}
Tree path array
↓のようなPath型を作成
declare const example: {
foo: {
bar: {
a: string
}
baz: {
b: number
c: number
}
}
}
type p = Path<typeof example>
// ["foo"] | ["foo", "bar"] | ["foo", "bar", "a"] | ["foo", "baz"] | ["foo", "baz", "b"] | ["foo", "baz", "c"]
解答
pathを保存するための配列を用意しておき、オブジェクトの各プロパティを見ていってプロパティの値がオブジェクトだったら配列にプロパティ名を入れて再帰的にPathを実行し、そうでない場合は配列に保存しておいたそれまでのpathと現在見ているプロパティ名を返す。
type Path<T, Ret extends unknown[] = [], K extends keyof T = keyof T> = K extends K
? T[K] extends Record<string, unknown>
? [...Ret, K] | Path<T[K], [...Ret, K]>
: [...Ret, K]
: never
他の人の解答
もっと簡潔な解き方をしている人がいた https://github.com/type-challenges/type-challenges/issues/18292
美しい
type Path<T> = T extends Record<PropertyKey, unknown>
? {
[P in keyof T]: [P, ...Path<T[P]>] | [P];
}[keyof T]
: never;
Is Negative Number
負の数かどうかを判定。
例外としてnumberを与えられた時とunionを与えられたときはneverを返す
解答
ほとんどポイントは例外を処理できるかどうかだけ。負の数かどうかは文字列に変換したときにマイナスが先頭につくかどうかで判定。
type IsUnion<T, U = T> = T extends T
? [U] extends [T]
? false
: true
: never
type IsException<T extends number> = Equal<T, number> extends true
? true
: IsUnion<T> extends true
? true
: false
type IsNegativeNumber<T extends number> = IsException<T> extends true
? never
: `${T}` extends `-${any}`
? true
: false
これにて上級編終了。
SimpleVueやUnionToTuple, UnionToIntersectionみたいな初見では何も思いつかないみたいなやつ以外はほとんど自力でできた。
最後の方はだいぶスピーディーに実装できていたので成長を感じた。