TypeChallenge-extra

Get Readonly Keys
問題文
オブジェクトの読み取り専用キーの Union を返す汎用的な GetReadonlyKeys<T> を実装してください。
interface Todo {
readonly title: string
readonly description: string
completed: boolean
}
type Keys = GetReadonlyKeys<Todo> // expected to be "title" | "description"
回答
type GetReadonlyKeys <
T,
U extends Readonly <T> = Readonly <T> ,
K extends keyof T = keyof T
> = K extends keyof T // ①
? Equal<Pick<T, K> ,Pick<U, K>> extends true
? K
: never
: never;
interface Todo {
readonly title: string
readonly description: string
completed: boolean
}
まとめ
- 各プロパティについて、読み取り専用でないバリアントで非等価チェックを行い、読み取り専用のプロパティだけに絞り込み、その結果からキーを読み取る。
- 今回は結果をUnionにしたいため、Union分配をする方法でアプローチします。
①の箇所がユニオンの分配になってます。
Todo
を分配すると
=>('title' | 'description' | 'completed') extends ('title' | 'description' | 'completed')
=>('title' extends ('title' | 'description' | 'completed')) | ('description' extends ('title' | 'description' | 'completed')) | ('completed' extends ('title' | 'description' | 'completed'))
になりあとは、Equal<Pick<T, K> ,Pick<U, K>> extends true
で評価する。
=> (Equal<Pick<Todo, 'title'>, Pick<ReadOnlyTodo, 'title'>> extends true) | (Equal<Pick<Todo, 'description'>, Pick<ReadOnlyTodo, 'description'>> extends true) | (Equal<Pick<Todo, 'completed'>, Pick<ReadOnlyTodo, 'completed'>> extends true))
別解
type GetReadonlyKeys<T> = { [ P in keyof Required<T> ]: Equal<{ [k in P]: T[k]}, { -readonly [R in P]: T[R] }> extends true ? never: P }[keyof T]
@dqn
さんの回答
これはオブジェクトを最後にUnionにする方法。 {...}[keyof T]
でUnionにしている。

Query String Parser
問題
あなたは、URLクエリー文字列をオブジェクト・リテラル型にパースする型レベルのパーサーを実装する必要があります。
いくつかの詳細な要件があります:
クエリー文字列のkeyの値は無視してもtrueにパースされます。例えば、'key'には値がないので、パーサーの結果は{ key: true }となります。
重複するキーは1つにまとめなければなりません。同じキーで異なる値がある場合、値はタプル型にマージされなければなりません。
キーが1つの値しか持たない場合、その値をタプル型にラップすることはできない。
同じキーを持つ値が複数回現れる場合、それは1回として扱われなければならない。例えば、key=value&key=valueは、key=valueとしてのみ扱われなければならない。
回答
type Includes<R extends any[], Value> = Value extends R[number] ? true : false
type AddProperty2<O extends object, K extends PropertyKey, V extends string | boolean> = K extends keyof O
? { [Key in keyof O] : K extends Key
? O[Key] extends any[]
? Includes<O[Key], V> extends false
? [...O[Key], V]
: O[Key]
: V extends O[Key] ? O[Key] : [O[Key], V]
: O[Key]
}
: { [Key in keyof O | K]: Key extends keyof O ? O[Key]: V };
type AddProperty<K extends PropertyKey, V extends string | boolean, Result extends object> = K extends keyof Result
? {
[P in keyof Result]: K extends P
? Result[P] extends any[]
? Includes<Result[P], V> extends true
? Result[P]
: [...Result[P], V]
: V extends Result[P]
? Result[P]
: [Result[P], V]
: Result[P]
}
: {
[P in keyof Result | K]: P extends keyof Result ? Result[P] : V
}
type ParseKey<KV extends string, Result extends object> = KV extends ''
? Result
: KV extends `${infer K}=${infer V}`
? AddProperty<K, V, Result>
: AddProperty<KV, true, Result>
type ParseQueryString<S extends string, Result extends object = {}> = S extends `${infer KV}&${infer Rest}`
? ParseQueryString<Rest, ParseKey<KV, Result>>
: ParseKey<S, Result>
まとめ
- 処理が多いため3つに分けて考える。
- ParseQueryString: クエリの文字列から1組のKVを取得する
- 例:
Key1=Value1&Key2=Value2&...
からKey1=Value1
を取得
- ParseKey: 1組のKV文字列から
Key
とValue
を取得
- 例:
Key1=Value1
からKey1
とValue1
を取得
- AddProperty:
Key
とValue
を結果に追加する
補足
PropertyKey
TypeScriptの組み込み型の1つであり、文字列またはシンボル型を表す型の合併型。文字列型はオブジェクトのプロパティ名として使われ、シンボル型はシンボルという一意の識別子を表す。
PropertyKey は JavaScript オブジェクトのプロパティ名として有効なすべての値を表します。

Slice
問題
JavaScriptのArray.slice関数を型システムで実装する。Slice<Arr, Start, End>は3つの引数を取る。出力はインデックスStartからEndまでのArrの部分配列でなければならない。負数のインデックスは逆から数える。
type Arr = [1, 2, 3, 4, 5]
type Result = Slice<Arr, 2, 4> // expected to be [3, 4]
回答
type NumberToArray<N extends number, Result extends any[] = []> = Result['length'] extends N ? Result : NumberToArray<N, [...Result, any]>
type NegNumberToArray<
Len extends number,
N extends number,
ArrL extends any[] = NumberToArray<Len>,
ArrR extends any[] = []> = `${N}` extends `-${ArrL['length']}`
? ArrR
: ArrL extends [infer _, ...infer WholeAnysR]
? NegNumberToArray<Len, N, WholeAnysR, [any, ...ArrR]>
: []
type IsNegative<N extends number> = `${N}` extends `-${infer _}` ? true : false;
type ParseArray<Len extends number, N extends number> = IsNegative<N> extends true
? NegNumberToArray<Len, N>
: NumberToArray<N>
type SliceRec<Arr extends any[], Start extends any[], End extends any[]> =
Start extends [infer _, ...infer StartA]
? End extends [infer _, ...infer EndA]
? Arr extends [infer _, ...infer ArrA]
? SliceRec<ArrA, StartA, EndA>
: []
: []
: End extends [infer _, ...infer EndA]
? Arr extends [infer F, ...infer Rest]
? [F, ...SliceRec<Rest, Start, EndA>]
: []
: []
type Slice<Arr extends any[], Start extends number = 0, End extends number = Arr['length']> =
SliceRec<Arr, ParseArray<Arr['length'], Start>, ParseArray<Arr['length'], End>>;
まとめ
- 数値を配列に変換する。マイナスの値の場合、
Arr['length] - 数値
の要素数の配列を作成。
- 例) Slice<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2, 5> => SliceRec<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [any, any], [any, any, any, any, any]>
- 例) Slice<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 1, -2> => SliceRec<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [any], [any, any, any, any, any, any, any, any]>
- SliceRecを実行する
- SliceRec<Arr extends any[], Start extends any[], End extends any[]>
1️⃣ Startが空配列の場合、Arrの先頭要素を取得。
2️⃣ Start, End, Arrから先頭要素を取り除く。
3️⃣SliceRec<Arr - 1, Start - 1, End - 1>
を実行する。
例
-
-
Slice<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 2, 5> => SliceRec<[1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [any, any], [any, any, any, any, any]>
=>[]
-
-
-
SliceRec<[2, 3, 4, 5, 6, 7, 8, 9, 10], [any], [any, any, any, any]>
=>[]
-
-
-
SliceRec<[3, 4, 5, 6, 7, 8, 9, 10], [], [any, any, any]>
=>[3]
-
-
-
SliceRec<[4, 5, 6, 7, 8, 9, 10], [], [any, any]>
=>[3, 4]
-
-
-
SliceRec<[5, 6, 7, 8, 9, 10], [], [any]>
=>[3, 4, 5]
-
-
-
SliceRec<[6, 7, 8, 9, 10], [], []>
=>[3, 4, 5]
-

Integers Comparator
問題
型レベルの整数比較器を実装する。このように比較結果を示す列挙型を用意した:
aがbより大きい場合、型はComparison.Greaterでなければなりません。
aとbが等しい場合、型はComparison.Equalでなければならない。
aがbより小さい場合、型はComparison.Lowerでなければなりません。
回答
enum Comparison {
Greater,
Equal,
Lower,
}
type Comparator<A extends number | string, B extends number | string> =
`${A}` extends `-${infer AA}`
? `${B}` extends `-${infer BB}`
? ComparePositives<BB, AA>
: Comparison.Lower
: `${B}` extends `-${number}`
? Comparison.Greater
: ComparePositives<`${A}`, `${B}`>
// Compares two positive long numbers
type ComparePositives<A extends string, B extends string, ByLength = CompareByLength<A, B>> =
ByLength extends Comparison.Equal
? CompareByDigits<A, B>
: ByLength
// Compares two strings by length
type CompareByLength<A extends string, B extends string> =
A extends `${infer _AH}${infer AR}`
? B extends `${infer _BH}${infer BR}`
? CompareByLength<AR, BR>
: Comparison.Greater
: B extends `${infer _BH}${infer _BR}`
? Comparison.Lower
: Comparison.Equal
// Compares two positive long numbers of the same length
type CompareByDigits<A extends string, B extends string> =
`${A}|${B}` extends `${infer AH}${infer AR}|${infer BH}${infer BR}`
? CompareDigits<AH, BH> extends Comparison.Equal
? CompareByDigits<AR, BR>
: CompareDigits<AH, BH>
: Comparison.Equal
// Compares two digits
type CompareDigits<A extends string, B extends string> =
A extends B
? Comparison.Equal
: '0123456789' extends `${string}${A}${string}${B}${string}`
? Comparison.Lower
: Comparison.Greater
まとめ
- 処理が多いため5つに分けて考える。
- Compares two digits: 2つの数を比較する。 ※数は1桁。
- CompareByDigits: 2つの数を比較する。 ※2つの数は同じ桁数。
- 先頭の数を取得して同じ数かどうかを確認。同じ数の場合、2番目以降の数で
CompareByDigits<Aの2番目以降の数, Bの2番目以降の数>
- 同じではない場合、
Compares<Aの先頭の数, Bの先頭の数>
- CompareByLength: 2つの数の桁数を比較する
- ComparePositives: 2つの正数を比較する
- まず、同じ桁数か確認する
CompareByLength
。 - 同じ桁数の場合、
CompareByDigits
で数値比較 - 同じ桁数ではない場合、
CompareByLength
の結果をそのまま返す。(桁数の比較結果がそのまま数値比較の結果)
- Comparator: 2つの数を正の数 or 負の数かを判定して、ComparePositivesをコールする
補足
CompareDigits
BよりAが先にある場合Lower
になる。逆の場合はGreater
になる。
'0123456789' extends `${string}${A}${string}${B}${string}`

Currying 2
問題
カリー(currying)とは、複数の引数を取る関数を、それぞれが単一の引数を取る一連の関数に変換するテクニックのことである。
しかし、日常生活では、動的引数のカリーもよく使われる。例えば、Function.bind(this, [...params]) APIなどだ。
const func = (a: number, b: number, c: number) => {
return a + b + c
}
const bindFunc = func(null, 1, 2)
const result = bindFunc(3) // result: 6
従って、Currying 1に基づくと、動的引数のバージョンが必要になる:
const add = (a: number, b: number, c: number) => a + b + c
const three = add(1, 1, 1)
const curriedAdd = DynamicParamsCurrying(add)
const six = curriedAdd(1, 2, 3)
const seven = curriedAdd(1, 2)(4)
const nine = curriedAdd(2)(3)(4)
この課題では、DynamicParamsCurrying は、0 から複数の引数を持つ関数を取ることができます。返される関数は、少なくとも1つの引数を受け付ける。すべての引数を満たした場合、元の関数の戻り値の型を正しく返す必要があります。
回答
type Curry<A extends any[], Result, D extends unknown[] = []> =
A extends [infer H, ...infer R]
? R extends []
? (...args: [...D, H]) => Result
: ((...args: [...D, H]) => Curry<R, Result>) & Curry<R, Result, [...D, H]>
: () => Result
declare function DynamicParamsCurrying<A extends unknown[], R>(fn: (...args: A) => R): Curry<A, R>
まとめ
type Curry<A, Result, D>
の理解が難しいため中間処理を書きました。
Curry<[string, number, boolean], Result, []>
((...args: [string]) =>Curry<[string], Result>) & Curry<[number, boolean], [], [string]>
((...args: [string]) =>Curry<[string], Result>) & ((...args: [string, number]) =>Curry<[number], Result>) & Curry<[boolean], [], [string, number]>
((...args: [string]) =>Curry<[string], Result>) & ((...args: [string, number]) =>Curry<[number], Result>) & ((...args: [string, number, boolean]) => Result)

Sum
問題
2つの負でない整数の和をとり、その和を文字列として返すSum<A, B>型を実装する。数値は文字列、数値、bigintで指定できる。
例えば
type T0 = Sum<2, 3> // '5'
type T1 = Sum<'13', '21'> // '34'
type T2 = Sum<'328', 7> // '335'
type T3 = Sum<1_000_000_000_000n, '123'> // '1000000000123'
回答
type NumberToTuple<N extends number, T extends 1[] = []> = T['length'] extends N
? T
: NumberToTuple<N, [...T, 1]>;
type Reverse<S extends string> = S extends `${infer F}${infer R}`
? `${Reverse<R>}${F}`
: S
type ToNumber<T> = T extends number ? T : never;
type CarryOver<N extends number> =
`${N}` extends `${infer _}${infer __ extends number}` ? 1 : 0;
type LastDigit<N extends number> = `${N}` extends `${infer _}${infer D extends number}` ? D : N
type Add<A extends number, B extends number> = ToNumber<
[...NumberToTuple<A>, ...NumberToTuple<B>]['length']
>;
type StringSum<
A extends string,
B extends string,
C extends number = 0
> = A extends `${infer AH extends number}${infer AR}`
? B extends `${infer BH extends number}${infer BR}`
? `${LastDigit<Add<Add<AH, BH>, C>>}${StringSum<AR, BR, CarryOver<Add<Add<AH, BH>, C>>>}`
: `${LastDigit<Add<AH, C>>}${CarryOver<Add<AH, C>> extends 1 ? StringSum<AR, '', 1> : AR}`
: B extends `${infer BH extends number}${infer BR}`
? `${LastDigit<Add<BH, C>>}${CarryOver<Add<BH, C>> extends 1 ? StringSum<BR, '', 1> : BR}`
: C extends 0
? ''
: `${C}`
type Sum<
A extends string | number | bigint,
B extends string | number | bigint
> = Reverse<StringSum<Reverse<`${A}`>, Reverse<`${B}`>>>;
まとめ
- 加算の時は
数値 => 配列 => 数値
にして計算する - 配列を逆にしてから計算する。桁数を合わせて加算するのが簡単になるため
Reverse<StringSum<Reverse<
{B}{A}`>, Reverse<` >>>
- NumberToTuple: 数値を配列に変換する
- Reverse: 文字列を逆にする
- ToNumber: 文字列を数値に変換する
- CarryOver: 渡された数値が 10以上かどうか確認する
- LastDigit: 渡された数値の先頭を除いた数を返す。例)
LastDigit<12345> => 2345
- Add: 加算する。
数値 => 配列 => 数値
にして計算する。
補足
ToNumber
がない場合、下記のエラーが出る。[1, 2, 3]['Length'] は数値になるとは限らない。

Multiply
問題
この課題は 476 - Sum の続きである。この課題を始めるには、まずこの課題を終えて、それに基づいてコードを修正することを推奨する。
2つの非負整数を乗算し、その積を文字列として返す Multiply<A, B> 型を実装する。数値は文字列、数値、bigintで指定できる。
例えば
type T0 = Multiply<2, 3> // '6'
type T1 = Multiply<3, '5'> // '15'
type T2 = Multiply<'4', 10> // '40'
type T3 = Multiply<0, 16> // '0'
type T4 = Multiply<'13', '21'> // '273'
type T5 = Multiply<'43423', 321543n> // '13962361689'
回答
type Numeric = string | number | bigint
type Reverse<A extends Numeric> =
`${A}` extends `${infer AH}${infer AT}`
? `${Reverse<AT>}${AH}` : ''
type DigsNext = {'0': '1', '1': '2', '2': '3', '3': '4', '4': '5', '5': '6', '6': '7', '7': '8', '8': '9'}
type DigsPrev = {[K in keyof DigsNext as DigsNext[K]]: K}
type AddOne<A> =
A extends `${infer AH}${infer AT}`
? AH extends '9'
? `0${AddOne<AT>}`
: `${DigsNext[AH & keyof DigsNext]}${AT}`
: '1'
type SubOne<A> =
A extends `${infer AH}${infer AT}`
? AH extends '0'
? `9${SubOne<AT>}`
: `${DigsPrev[AH & keyof DigsPrev]}${AT}`
: never
type Add<A, B> =
A extends `${infer AH}${infer AT}`
? B extends `${infer BH}${infer BT}`
? BH extends '0' ? `${AH}${Add<AT, BT>}` : Add<AddOne<A>, SubOne<B>>
: A
: B
type Mul<A extends string, B extends string, R = '0'> =
A extends '0'
? R
: B extends '0'
? R
: A extends `${infer AH}${infer AT}`
? AH extends '0'
? Mul<AT, `0${B}`, R>
: Mul<SubOne<A>, B, Add<R, B>>
: R
type Multiply<A extends Numeric, B extends Numeric> =
Reverse<Mul<Reverse<A>, Reverse<B>>>
まとめ
- 実装の工夫が必要。
- 200 x 1234 を
200 + 200 + 200 + 200 + ...
と考えて200を1234回再帰して答えを出そうとするとTSの再帰数制限(1000)に引っかかる。 -
(200 x 1)000
+(200 x 2)00
+(200 x 3)0
+(200 x 4)
と実装すると再帰の数が減る。

Tag
問題
TypeScriptの構造的な型付けシステムにもかかわらず、いくつかの型をタグでマークし、タグがこれらの型の値を互いに代入する機能を妨げないようにすると便利なことがある。
例えば、タグを使用することで、ある値が必要な関数の呼び出しを正しい順序で通過することをチェックすることができる:
const doA = <T extends string>(x: T) => {
const result = x
return result as Tag<typeof result, 'A'>
}
const doB = <T extends string>(x: T) => {
const result = x
return result as Tag<typeof result, 'B'>
};
const a = doA('foo')
const b = doB(a)
type Check0 = IsTrue<HasTags<typeof b, ['A', 'B']>>
nullとundefined以外の型Bを受け取り、文字列リテラル型Tでラベル付けされた型を返す関数Tag<B, T extends string>を書く。
ラベル付けされた型は、対応する元の型と相互に代入可能でなければならない:
declare let x: string
declare let y: タグ<文字列, 'A'> x = y = x
x = y = x
すでにタグが付けられている型にタグを付ける場合、その型のすべてのタグのリストの最後に新しいタグを追加しなければならない:
type T0 = Tag<{ foo: string }, 'A'>
type T1 = Tag<T0, 'B'>
type Check1 = IsTrue<HasExactTags<T1, ['A', 'B']>>
HasTag<B, T extends string> は、タイプBがタグTでタグ付けされているかどうかをチェックする(そしてtrueかfalseを返す):
type T3 = Tag<0 | 1, 'D'>
type Check3 = IsTrue<HasTag<T3, 'D'>>
HasTags<B, T extends readonly string[]> は、B型がタプルTのタグで連続してタグ付けされているかどうかをチェックします。
type T4 = Tag<Tag<Tag<{}, 'A'>, 'B'>, 'C'>
type Check4 = IsTrue<HasTags<T4, ['B', 'C']>>
HasExactTags<B, T extends readonly string[]> は、B型のすべてのタグのリストがTタプルと正確に等しいかどうかをチェックします:
type T5 = Tag<Tag<unknown, 'A'>, 'B'>
type Check5 = IsTrue<HasExactTags<T5, ['A', 'B']>>
最後に、タイプBからすべてのタグを削除するタイプUnTag<B>を追加する:
type T6 = Tag<{ bar: number }, 'A'>
type T7 = UnTag<T6>
type Check6 = IsFalse<HasTag<T7, 'A'>>
回答
declare const uq: unique symbol
declare const dummy: unique symbol
type UQ = typeof uq
type DUMMY = typeof dummy
type UU<T> = Exclude<T, undefined>
type GetTags<B>
= Equal<B, any> extends true ? []
: UQ extends keyof B ? [Exclude<keyof UU<B[UQ]>, UQ | DUMMY>, ...GetTags<UU<B[UQ]>>] : []
type TagRec<B, T extends string>
= UQ extends keyof B ? Omit<B, UQ> & {[uq]?: TagRec<UU<B[keyof B & UQ]>, T>}
: B & {[uq]?: {[P in T | DUMMY]?: 1}}
type Tag<B, T extends string>
= Equal<B, null> extends true ? B
: Equal<B, undefined> extends true ? B
: Equal<B, any> extends true ? TagRec<{dummy: 1}, T>
: Equal<B, never> extends true ? TagRec<{dummy: 2}, T>
: TagRec<B, T>
type Member<T extends any[], U> = U extends T[number] ? true : false
type PrefixUnion<T> = T extends [...infer L, infer R] ? T | PrefixUnion<L> : never
type Subseq<T, U>
= U extends PrefixUnion<T> ? true
: T extends [infer L, ...infer R] ? Subseq<R, U> : false
type UnTag<B> = UQ extends keyof B ? Omit<B, UQ> : B
type HasTag<B, T extends string> = (B extends any ? Member<GetTags<B>, T> : never) extends true ? true : false
type HasTags<B, T extends readonly string[]> = Subseq<GetTags<B>, T>
type HasExactTags<B, T extends readonly string[]> = GetTags<B> extends T ? true : false
まとめ
-
unique symbol
を使ってunique型を宣言します。
declare const uq: unique symbol
declare const dummy: unique symbol: uq
-
type UU<T> = Exclude<T, undefined>: UU
Tからundefinedを除外します。これは、undefinedでない型を取得するための型エイリアスです。 -
type GetTags<B> = ...
型Bのタグを抽出します。この型は再帰的に呼び出され、B内のすべてのタグを配列として取得します。 -
type TagRec<B, T extends string> = ...
型Bにタグを追加または置き換えます。Tは新しいタグの名前を指定します。 -
type Tag<B, T extends string> = ...: Tag
型Bにタグを追加します。Tは新しいタグの名前を指定します。この型はさまざまな条件に基づいて異なるタグを付けます。 -
type Member<T extends any[], U> = ...
型Uが型Tの要素であるかどうかを判定します。 -
type PrefixUnion<T> = ...
型T内のすべての可能なプレフィックスを抽出します。 -
type Subseq<T, U> = ...
型Tが型Uのサブシーケンス(一部分として含まれるか)であるかどうかを判定します。
9 type UnTag<B> = ...
型Bからタグを削除します。
-
type HasTag<B, T extends string> = ...: HasTag
型Bが指定されたタグTを持つかどうかを判定します。 -
type HasTags<B, T extends readonly string[]> = ...
型Bが指定された一連のタグを持つかどうかを判定します。 -
type HasExactTags<B, T extends readonly string[]> = ...
型Bが指定された一連のタグを正確に持つかどうかを判定します。

InclusiveRange
問題
型システムにおける再帰の深さは、TypeScriptの制限のひとつである。
もっと深くする必要がある。そして、もっと深くすることもできる。
この課題では、自然数の範囲を包含的にスライスした、1つの下界と1つの上界が与えられている。どちらの境界も0から200まで変化するので、制限よりも深い再帰を行うことができるテクニックを開発する必要がある。
Lower > Higherの場合は、空のタプルを出力することに注意。
回答
enum Comparison {
Greater,
Equal,
Lower,
}
type Comparator<A extends number | string, B extends number | string> =
`${A}` extends `-${infer AA}`
? `${B}` extends `-${infer BB}`
? ComparePositives<BB, AA>
: Comparison.Lower
: `${B}` extends `-${number}`
? Comparison.Greater
: ComparePositives<`${A}`, `${B}`>
// Compares two positive long numbers
type ComparePositives<A extends string, B extends string, ByLength = CompareByLength<A, B>> =
ByLength extends Comparison.Equal
? CompareByDigits<A, B>
: ByLength
// Compares two strings by length
type CompareByLength<A extends string, B extends string> =
A extends `${infer _AH}${infer AR}`
? B extends `${infer _BH}${infer BR}`
? CompareByLength<AR, BR>
: Comparison.Greater
: B extends `${infer _BH}${infer _BR}`
? Comparison.Lower
: Comparison.Equal
// Compares two positive long numbers of the same length
type CompareByDigits<A extends string, B extends string> =
`${A}|${B}` extends `${infer AH}${infer AR}|${infer BH}${infer BR}`
? CompareDigits<AH, BH> extends Comparison.Equal
? CompareByDigits<AR, BR>
: CompareDigits<AH, BH>
: Comparison.Equal
// Compares two digits
type CompareDigits<A extends string, B extends string> =
A extends B
? Comparison.Equal
: '0123456789' extends `${string}${A}${string}${B}${string}`
? Comparison.Lower
: Comparison.Greater
type NumberToTuple<N extends number, T extends 1[] = []> = T['length'] extends N
? T
: NumberToTuple<N, [...T, 1]>;
type PlusOne<N extends number> = ToNumber<[...NumberToTuple<N>, any]['length']>
type ToNumber<T> = T extends number ? T : never;
type InclusiveRange<Lower extends number, Higher extends number, Result extends number[] = [], Count extends number = Lower> =
Comparator<`${Count}`, `${Higher}`> extends Comparison.Greater
? Result
: InclusiveRange<Lower, Higher, [...Result, Count], PlusOne<Count>>
まとめ
- Graterの実装を使うとシンプルにできる。
- InclusiveRangeのCount引数の初期値をLowerと同じにすることでCountとLowerの比較がなくなり動作が軽くなる。

Sort
問題
この課題では、自然数の配列を昇順または降順のいずれかでソートすることが求められる。
昇順の例
Sort<[]> // []
Sort<[1]> // [1]
Sort<[2, 4, 7, 6, 6, 6, 5, 8, 9]> // [2, 4, 5, 6, 6, 6, 7, 8, 9]
Sort型はboolean型も受け付けるべきである。これが真の場合、ソート結果は降順になる。いくつかの例を挙げよう:
Sort<[3, 2, 1], true> // [3, 2, 1]
Sort<[3, 2, 0, 1, 0, 0, 0], true> // [3, 2, 1, 0, 0, 0, 0]
15桁以上の自然数をサポートする。
浮動小数点数のサポート。
回答
type Numeric = number | bigint | string
type s = string;
type Length<S extends Numeric, Counter extends 1[] = []> =
`${S}` extends `${s}${s}${s}${s}${s}${
s}${s}${s}${s}${s}${infer Rest}`
? Length<Rest, [...Counter, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]>
: `${S}` extends `${s}${infer Rest}`
? Length<Rest, [...Counter, 1]>
: Counter['length']
/**
* Determine if digit is greater than comparator
*
* @example
* type Greater = GreaterThanDigit<9, 0> // true
* type Less = GreaterThanDigit<0, 9> // false
* type Equal = GreaterThanDigit<0, 0> // false
*/
type GreaterThanDigit<X extends Numeric, Y extends Numeric> =
`0123456789` extends `${s}${Y}${s}${X}${s}`
? true
: false
/**
* Determine if numeric value is greater than
* comparator of equal length
*
* @example
* type Greater = GreaterThanDigits<9, 0> // true
* type Less = GreaterThanDigits<0, 9> // false
* type Equal = GreaterThanDigits<0, 0> // false
* type Multiple = GreaterThanDigits<20, 10> // true
*/
type GreaterThanDigits<X extends Numeric, Y extends Numeric> =
`${X}` extends `${infer X1}${infer XR}`
? `${Y}` extends `${infer Y1}${infer YR}`
? X1 extends Y1
? GreaterThanDigits<XR, YR>
: GreaterThanDigit<X1, Y1>
: never
: GreaterThanDigit<X, Y>
/**
* Determine if numeric value is greater than
* comparator of varying length
*
* @example
* type Greater = GreaterThan<9, 0> // true
* type Less = GreaterThan<0, 9> // false
* type Equal = GreaterThan<0, 0> // false
* type Multiple = GreaterThan<20, 10> // true
* type VaryingLength = GreaterThan<9, 10> // true
*/
type GreaterThan<
X extends Numeric,
Y extends Numeric,
XLen extends number = Length<X>,
YLen extends number = Length<Y>
> = XLen extends YLen
? GreaterThanDigits<X, Y>
: GreaterThan<XLen, YLen>
type InsertSorted<
Entries,
Entry extends Numeric,
Descending extends boolean = false
> =
Entries extends [
infer Current extends Numeric,
...infer Rest,
]
// Less than or equal condition if descending flag is false
? GreaterThan<Entry, Current> extends Descending
? [Entry, ...Entries]
: [Current, ...InsertSorted<Rest, Entry, Descending>]
: [Entry]
type Sort<
Arr,
Descending extends boolean = false,
Sorted = []
> = Arr extends [infer H extends Numeric, ...infer R]
? Sort<R, Descending, InsertSorted<Sorted, H, Descending>>
: Sorted
まとめ
- 特に難しい箇所はないが再帰の制限回避のために
GreaterThanDigit
の実装箇所で工夫している。10回再起を1回の再起でいいように工夫している.

DistributeUnions
問題
Distributeユニオン型を実装し、ユニオン型を含むデータ構造の型を、ユニオンを含まない許容されるデータ構造のすべての可能な型のユニオンに変える。データ構造は、任意の入れ子レベルのオブジェクトとタプルの任意の組み合わせとすることができる。
例えば
type T1 = DistributeUnions<[1 | 2, 'a' | 'b']>
// => [1, 'a'] | [2, 'a'] | [1, 'b'] | [2, 'b']
type T2 = DistributeUnions<{ type: 'a', value: number | string } | { type: 'b', value: boolean }>
// => | { type 'a', value: number }
// | { type 'a', value: string }
// | { type 'b', value: boolean }
type T3 = DistributeUnions<[{ value: 'a' | 'b' }, { x: { y: 2 | 3 } }] | 17>
// => | [{ value: 'a' }, { x: { y: 2 } }]
// | [{ value: 'a' }, { x: { y: 3 } }]
// | [{ value: 'b' }, { x: { y: 2 } }]
// | [{ value: 'b' }, { x: { y: 3 } }]
// | 17
コンテキストの場合、この型は深いデータ構造上のケースを除外したい場合に非常に便利である:
type DistributeUnions<T>
= T extends unknown[]
? DistributeArray<T>
: T extends object
? Merge<DistributeObject<T>>
: T
type DistributeArray<Arr extends unknown[]> =
Arr extends [infer H, ...infer R]
? ArrHelper<DistributeUnions<H>, R>
: []
type ArrHelper<H, T extends unknown[]> =
H extends H
? [H, ...DistributeArray<T>]
: never
type DistributeObject<O extends object, K extends keyof O = keyof O>
= [K] extends [never]
? {}
: K extends K
? ObjHelper<K, DistributeUnions<O[K]>> & DistributeObject<Omit<O, K>>
: never
type ObjHelper<K extends PropertyKey, V> =
V extends V
? { [k in K]: V }
: never
type Merge<O> = { [K in keyof O]: O[K] }
まとめ
-
DistributeUnions:与えられた型 T を処理します。もし T が配列型(unknown[])の場合、 DistributeArray を呼び出して配列内の要素を処理し、オブジェクト型の場合は DistributeObject を呼び出してオブジェクト内のプロパティを処理します。それ以外の型の場合は T をそのまま返します。
-
DistributeArray:共用型内の配列を処理します。最初の要素(H)を DistributeUnions で処理し、残りの要素(T)に再帰的に DistributeArray を適用します。展開された配列は、新しい共用型として返されます。
-
ArrHelper:これは DistributeArray の内部で使用され、配列の要素を処理します。
H extends H
してUnionの場合、Distributionします。
- 例: type A = ArrHelper<'3' | '4', ['1', '2']>
- => '3' | '4' extends '3' | '4'
- => ['3', ...DistributeArray<['1', '2']>] | ['4', ...DistributeArray<['1', '2']>]
- => ['3', '1', '2'] | ['4', '1', '2']
- DistributeObject:この型は、共用型内のオブジェクトを処理します。まず、オブジェクト内のプロパティのキー(K)を取得し、それぞれのプロパティの値を DistributeUnions で処理します。展開されたオブジェクトは、新しい共用型として返されます。
-
例: DistributeObject<{ type1: 'a' , type2: 'b'}>
-
=> ['type1' | 'type2'] extends [never]
-
=> ObjHelper<'type1' | 'type2', DistributeUnions<'a' | 'b'>> & DistributeObject<Omit<{ type1: 'a' , type2: 'b'}, 'a' | 'b'>>
-
({
type1: "a";
} & {
type2: "b";
}) | ({
type2: "b";
} & {
type1: "a";
})
※ [K] extends [never] はnever判定する機能。分散しないようにするため[]を使用する。
-
ObjHelper:これは DistributeObject の内部で使用され、オブジェクトのプロパティを処理します。プロパティ名(K)とプロパティの値(V)を使用して新しいオブジェクトを生成します。
-
Merge:展開されたオブジェクトを単一のオブジェクトにマージします。

Subtract
問題
BuildTupleを使って、JavascriptでSubtraction型を実装する。
もしminuendがsubtrahendより小さければ、それは決してないはずである。
簡単なバージョンだ。
例えば
Subtract<2, 1> // expect to be 1
Subtract<1, 2> // expect to be never
回答
type Tuple<T, Result extends 1[] = []> = 0 extends 1
? never
: Result['length'] extends T
? Result
: Tuple<T, [...Result, 1]>;
type Subtract<M extends number, S extends number> =
Tuple<M> extends [...Tuple<S>, ...infer Rest]
? Rest['length']
: never;
まとめ
- 計算量が1回でできるように
Tuple<M> extends [...Tuple<S>, ...infer Rest]
している。 - 例) Subtract<10, 5>の場合
- Subtract<10, 5>
=> Tuple<10> extends [...Tuple<5>, ...infer Rest]
=> [1, 1, 1, 1, ,1, 1, 1, 1, 1] extends [1, 1, 1, 1, 1, ...infer Rest]
=> ? Rest['length']
=> ?[1, 1, 1, 1, 1]['length']

Assert Array Index
問題
TypeScriptは、配列の要素に実際のインデックスでアクセスしているかどうか(配列の長さを超えていないかどうか)、インデックスとして任意の数値を使っていないかどうか、別の配列からのインデックスを使っていないかどうか(入れ子ループや行列やグラフの走査の場合)を一切チェックしない:
const matrix = [
[3, 4],
[5, 6],
[7, 8],
];
// This example contains no type errors when the noUncheckedIndexedAccess option is off.
for (let i = 0; i < matrix.length; i += 1) {
const columns: number[] = matrix[i];
for (let j = 0; j < columns.length; j += 1) {
const current: number = columns[i]; // oops! i instead of j
console.log(
current.toFixed(), // TypeError: Cannot read property 'toFixed' of undefined
);
}
}
任意の配列(型レベルで配列を区別するために必要な、任意の一意な文字列キーを持つ)に適用できるassert関数assertArrayIndex(array, key)を書いて、特別な汎用型Index<typeof array>によって配列から得られるインデックスによってのみ、この配列の要素にアクセスできるようにする(この機能は、tsconfig.jsonでnoUncheckedIndexedAccessオプションを有効にする必要がある):
const numbers = [5, 7];
assertArrayIndex(numbers, 'numbers');
for (let i = 0 as Index<typeof numbers>; i < numbers.length; i += 1) {
console.log(numbers[i].toFixed());
}
このようなインデックスによってアクセスする場合、配列内の要素が存在することが保証されなければならず、他のインデックスによって配列にアクセスする場合、そのような保証はない(要素が存在しないかもしれない):
const matrix = [
[3, 4],
[5, 6],
[7, 8],
];
assertArrayIndex(matrix, 'rows');
let sum = 0;
for (let i = 0 as Index<typeof matrix>; i < matrix.length; i += 1) {
const columns: number[] = matrix[i];
// @ts-expect-error: number | undefined in not assignable to number
const x: number[] = matrix[0];
assertArrayIndex(columns, 'columns');
for (let j = 0 as Index<typeof columns>; j < columns.length; j += 1) {
sum += columns[j];
// @ts-expect-error: number | undefined in not assignable to number
const y: number = columns[i];
// @ts-expect-error: number | undefined in not assignable to number
const z: number = columns[0];
// @ts-expect-error: number[] | undefined in not assignable to number[]
const u: number[] = matrix[j];
}
}
assertArrayIndex関数はタプルでは呼び出すことができません(要素へのアクセスはすでに型付けされているため):
const tuple = [5, 7] as const;
// @ts-expect-error
assertArrayIndex(tuple, 'tuple');
回答
type HashMap = {
a:1;
b:2;
c:3;
d:4;
e:5;
f:6;
g:7;
h:8;
i:9;
j:10;
k:11;
l:12;
m:13;
n:14;
o:15;
p:16;
q:17;
r:18;
s:19;
t:20;
u:21;
v:22;
w:23;
x:24;
y:25;
z:26;
};
type ToArray<N, Result extends any[] = []>
= Result['length'] extends N
? Result
: ToArray<N, [[], ...Result]>
type Hash<S, A extends any[] = []>
= S extends `${infer L}${infer R}`
? Hash<R, [...ToArray<HashMap[keyof HashMap & L]>, ...A]>
: A['length']
function assertArrayIndex<A extends readonly any[], K extends string>(
array: number extends A['length'] ? A : never,
key: K
) : asserts array is (
number extends A['length']
? A & {key: Hash<K>} & {[P in Hash<K>]: A[number]}
: never
) {}
type Index<Array> = Array[keyof Array & 'key']
まとめ
-
HashMap:アルファベット文字をキー(a-z)とし、1から26の数値とするオブジェクト型です。アルファベットをキーとして対応する数字を取得するために使用されます。
-
ToArray :指定された長さ N の配列を生成します。再帰的に [[], ...Result] を Result 配列に追加することで、指定された長さまでの配列を生成します。
-
Hash :文字列 S を受け取り、各文字を HashMap から対応する数値に変換して配列 A に追加します。文字列 S を分解して数値の配列を生成します。最後は数値(配列の長さ)を返します。
例1)HashMap<'a'>
=>1
例2)HashMap<'b'>
=>2
例3)HashMap<'c'>
=>3
例4)HashMap<'abc'>
=>6(1 + 2 + 3)
-
assertArrayIndex:配列 A とキー K を受け取り、配列 A が特定の条件を満たすかどうかを検証します。条件は、number extends A['length'] というもので、配列 A の長さが number 型である場合に検証が行われます。検証が成功すると、特定の型が返されます。
-
Index:配列型 Array の 'key' プロパティにアクセスすることで、ハッシュマップのキーに対応する数値を取得します。

JSONParser
問題
JSON文字列をオブジェクトリテラル型にパースするために、型レベルの部分的なパーサーを実装する必要があります。
要件
JSON内の数字やUnicodeエスケープ( \uxxxx) は無視できます。解析する必要はありません。
回答
自分では解けなかったため、@okayu29 さんの回答を見ながら自分なりにまとめたいと思います。
type Parse<T extends string> = Eval<T> extends [infer V, infer U] ? V : never
type Eval<T> = T extends `${' '|'\n'}${infer U}`
? Eval<U>
: T extends `true${infer U}`
? [true, U]
: T extends `false${infer U}`
? [false, U]
: T extends `null${infer U}`
? [null, U]
: T extends `"${infer U}`
? EvalString<U>
: T extends `[${infer U}`
? EvalArray<U>
: T extends `{${infer U}`
? EvalObject<U>
: false
type Escapes = {r:'\r', n:'\n', b:'\b', f:'\f'}
type EvalString<T, S extends string = ''> = T extends `"${infer U}`
? [S, U]
: (T extends `\\${infer C}${infer U}`
? C extends keyof Escapes
? [C, U]
: false
: false
) extends [infer C, infer U]
? EvalString<U, `${S}${C extends keyof Escapes ? Escapes[C] : never}`>
: T extends `${infer C}${infer U}`
? EvalString<U, `${S}${C}`>
: false
type EvalArray<T, A extends any[] = []>
= T extends `${' '|'\n'}${infer U}`
? EvalArray<U, A>
: T extends `]${infer U}`
? [A, U]
: T extends `,${infer U}`
? EvalArray<U, A>
: Eval<T> extends [infer V, infer U]
? EvalArray<U, [...A, V]>
: false
type EvalObject<T, K extends string = '', O = {}> = T extends `${' '|'\n'}${infer U}`
? EvalObject<U, K, O>
: T extends `}${infer U}`
? [O, U]
: T extends `,${infer U}`
? EvalObject<U, K, O>
: T extends `"${infer U}`
? Eval<`"${U}`> extends [`${infer KK}`, infer UU]
? EvalObject<UU, KK, O>
: false
: T extends `:${infer U}`
? Eval<U> extends [infer V, infer UU]
? EvalObject<UU, '', Merge<{[P in K]: V} & O>>
: false
: false
type Merge<T> = {[P in keyof T]: T[P]}
まとめ
「EvalString」「EvalArray」「EvalObject」に場合分けして地道に文字列比較で処理する。
特にこれといって特殊なテクニックはないです。

typeof
演算子。
型を示す文字列を返す。

BigInt と Numberの違い
大きすぎて number プリミティブで表すことができない数を、表現したり操作したりするために使用する。
Numberと異なる点
- 組み込みの Math オブジェクト内のメソッドでは利用できない
- 演算で Number の値と混ぜることができない(同じ型に統一する必要がある)
- BigInt を Number へ変換する際には精度が落ちることがある
比較演算
- BigInt 値は Number 値と厳密等価ではありませんが、等価にはなる。
0n === 0
// ↪ false
0n == 0
// ↪ true
- Number と BigInt は通常通り比較できる。
1n < 2
// ↪ true
2n > 1
// ↪ true
2 > 2
// ↪ false
2n > 2
// ↪ false
2n >= 2
// ↪ true
Number型の最大値
Number.MAX_SAFE_INTEGER(整数の最大値 (2^53 - 1) )
※比較はちゃんと動かない。
const x = Number.MAX_SAFE_INTEGER + 1;
const y = Number.MAX_SAFE_INTEGER + 2;
console.log(Number.MAX_SAFE_INTEGER);
// Expected output: 9007199254740991
console.log(x);
// Expected output: 9007199254740992
console.log(x === y);
// Expected output: true

Omit<T, K>
備考
type MyOmit<T, K extends keyof T> = {
[P in keyof T as P extends K ? never : P]: T[P]
}

型アサーション as
型推論を上書きする機能(型アサーション)
型アサーションの書き方は2つ
1つ目はas構文
const value: string | number = "this is a string";
const strLength: number = (value as string).length;
2つ目はアングルブラケット構文
const value: string | number = "this is a string";
const strLength: number = (<string>value).length;
型アサーションとキャストの違い
キャストとは、実行時にある値の型を別の型に変換することです。
型アサーションは、実行時に影響しません。値の型変換はしない。あくまでコンパイル時にコンパイラーに型を伝えるだけ。

Readonly<T>
オブジェクト型Tのプロパティを全て読み取り専用にするユーティリティ型。
※ Readonlyの効果は再帰的ではない。
※ Readonly<T>が読み取り専用にするのは、オブジェクト型T直下のプロパティのみ。
Readonly<T>もどきを実装すると下記のようになる。(TypeChallenge)
type MyReadonly2<T, K extends keyof T = keyof T> = {
readonly [P in K]: T[P]
} & {
[P in Exclude<keyof T, K>]: T[P]
}
type MyReadonly2<T, K extends keyof T = keyof T> = {
[P in keyof Omit<T, K>]: T[P]
} & {
readonly [P in keyof Pick<T, K>]: T[P]
}
type MyReadonly2<T, K extends keyof T = keyof T> = Omit<T, K> & Readonly<T>;

DeepReadonly<T>
TypeChallengeの問題を解く。
type DeepReadonly<T> = {
readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>
}

Tuple to Union
type TupleToUnion<T extends any[]> = T[number]

Chainable
type Chainable<T extends object = {}> = {
option<K extends string, V>(key: K, value: V): Chainable<T & { [p in K]: V}>
get(): T
}
type Chainable<T = {}> = {
option: <K extends string, V>(key: K extends keyof T
? never : K, value: V
) => Chainable<Omit<T, K> & Record<K, V>>,
get: () => T
}

Last of Array
type Last<T extends any[]> = T extends [...any, infer L] ? L : never

Variadic Tuple Types

Pop
type Pop<T extends any[]> = T extends [...infer H, any] ? H : []

Promise.all
関連技術
-
as const(const assertion)
- 再帰的にreadonlyにできる
type PromiseFlat<T> = T extends Promise<infer R> ? PromiseFlat<R> : T
declare function PromiseAll<T extends any[]>(
values: readonly [...T]
): Promise<{
[P in keyof T]: PromiseFlat<T[P]>
}>
解説
Promise<T> から T を取り出すには
T extends Promise<infer R> ? R : T
Promiseがネストしたケースに対応するため、Promise型にマッチした場合、再度PromiseFlatをコールする。
type PromiseFlat<T> = T extends Promise<infer R> ? PromiseFlat<R> : T
Promiseがネストしたケース

LookUp
type LookUp<U, T> = U extends { type: T } ? U : never
type LookUp<U, T extends string> = {
[P in T]: U extends { type: T } ? U : never
}[T];

Distributive Conditional Types
T extends U ? X : Y
で T = A | B | C
の時、
(A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
になる。

Permutation
- Distributive conditional types
- [U] extends [never] ? [] : ...
type Permutation<T, C=T> = [T] extends [never]
? []
: T extends any
? [T, ...Permutation<Exclude<C, T>>]
: never

Length of String
- Arrayは
length
がある
type StringToArray<S extends string> = S extends `${infer H}${infer T}`
? [H, ...StringToArray<T>]
: []
type LengthOfString<S extends string> = StringToArray<S>['length']
Array と Stringの lengthプラパティの挙動は異なる

Flatten
T extends [infer H, ...infer R]
を使うのが肝。
あとはひたすら展開。
type Flatten<T> = T extends []
? []
: T extends [infer H, ...infer R]
? [...Flatten<H>, ...Flatten<R>]
: [T]

Append to object
type AppendToObject<T extends object, U extends string, V> = {
[P in keyof T | U]: P extends keyof T ? T[P] : V
}
type AppendToObject<
T extends Record<PropertyKey, any>,
U extends PropertyKey,
V,
R = (T & Record<U, V>),
> = { [Key in keyof R]: R[Key] }

Absolute
数値は文字列に変換して extends
するのが重要。
type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer R}` ? R : `${T}`

String to Union
1文字ずつ取り出すのが重要。
type StringToUnion<T extends string> = T extends `${infer H}${infer R}` ? H | StringToUnion<R> : never

KebabCase
- 大文字の判定ロジック
type IsUpperCase<T extends string> = Uppercase<T> extends T
? Lowercase<T> extends T
? false
: true
: false
下記は一見正しいように見えるが大文字・小文字の区別がない文字の場合 Uppercase<T> extends T
もtrueになるため本当に渡された文字が本当に大文字なのか Uppercase<T> extends T
だけでは判断できない。
そのため、Uppercase<T> extends T
と Lowercase<T> extends T
両方 trueになるものは大文字・小文字区別がない文字と判断している。
type IsUpperCase<T extends string> = Uppercase<T> extends T
: true
: false
// IsUpperCase<'-'> => '-'
// IsUpperCase<'='> => '='
// IsUpperCase<'😎'> => '😎'
type IsUpperCase<T extends string> = Uppercase<T> extends T
? Lowercase<T> extends T
? false
: true
: false
type KebabCase<S> = S extends `${infer H}${infer M}${infer R}`
? true extends IsUpperCase<M>
? `${Lowercase<H>}-${KebabCase<`${Lowercase<M>}${R}`>}`
: `${Lowercase<H>}${KebabCase<`${M}${R}`>}`
: S

Diff
type Diff<O extends object, O1 extends object> = {
[P in Exclude<keyof O, keyof O1> | Exclude<keyof O1, keyof O>]
: P extends keyof O
? O[P]
: P extends keyof O1
? O1[P]
: never
}

AnyOf
type IsFalthy = false | 0 | Record<string, never> | undefined | [] | '' | null
を定義するのが重要。
何がFaltyなのかをUnion型で定義。その後、配列から要素を取得して IsFalthy
と比較していく。
type IsFalthy = false | 0 | Record<string, never> | undefined | [] | '' | null
type AnyOf<T extends readonly any[]> = T extends [infer H, ...infer R]
? H extends IsFalthy
? AnyOf<R>
: true
: false

neverについて
-
never
は共用体ではない。空集合。

unknown型
anyに近いが安全なunknown型.
何でも入れられる型。
print関数には何でも渡すことができる。
function print(val: unknown) {
console.log(val)
}
print(3)
print({ user: { name: 'hoge' }})
any型との違い
- 値を使う側にある。
- any型は一切型のチェックを行わない。※any型の危険の源
- unknown型はコンパイラが「不明」である性質をリスペクトする
- リスペクトするため、できることが非常に限られる。例えば、下記のように
val.name
にアクセスするとコンパイラでエラーになる。 val変数がnull or undefined or number
の可能性があるのでエラーになる。
unknown型はどのように扱えばいいのか?
型の絞り込みを行なって処理する。
function useUnknown(val: unknown) {
if (typeof val === 'string') {
console.log('valは文字列です')
console.log(val.slice(0, 5))
} else {
console.log('valは文字列以外です')
console.log(val)
}
}
参考

Replace Key
type ReplaceKeys<U, T, Y> = {
[P in keyof U]: P extends T
? P extends keyof Y
? Y[P]
: never
: U[P]
}

RemoveIndexSignature<T>
TypeLiteralOnly
の判定ロジックをかけるかが重要。
type TypeLiteralOnly<T> =
string extends T
? never
: number extends T
? never
: symbol extends T
? never
: T
type RemoveIndexSignature<T> = {
[P in keyof T as TypeLiteralOnly<P>]: T[P]
}

PercentageParser
3つの部分に分解する。「ParseSign」「ParsePercentage」「ParseNumber」
ParseNumberのこの記述が書けるかどうかが重要。S extends ${ParseSign<S>}${infer N}${ParsePercentage<S>}
type ParseSign<S extends string> = S extends `-${string}`
? '-'
: S extends `+${string}`
? '+'
: ''
type ParsePercentage<S extends string> = S extends `${string}%`
? '%'
: ''
type ParseNumber<S extends string> = S extends `${ParseSign<S>}${infer N}${ParsePercentage<S>}`
? N
: ''
type PercentageParser<A extends string> = [ParseSign<A>, ParseNumber<A>, ParsePercentage<A>]
type ReverseStr<S> = S extends `${infer H}${infer R}`
? `${ReverseStr<R>}${H}`
: ''
type IsSign = '+' | '-'
type GetSign<S extends string> = S extends `${infer H}${infer R}`
? H extends IsSign
? H
: ""
: ""
type IsPercentage = '%'
type GetPercentage<S extends string> = ReverseStr<S> extends `${infer H}${infer R}`
? H extends IsPercentage
? H
: ''
: ''
type GetNumber<S extends string, RemoveStr = GetPercentage<S> | GetSign<S>> = S extends `${infer H}${infer R}`
? H extends RemoveStr
? GetNumber<R>
: `${H}${GetNumber<R>}`
: ''
type PercentageParser<S extends string> = [GetSign<S>, GetNumber<S>, GetPercentage<S>]

DropChar
type DropChar<S extends string, C extends string> = S extends `${infer H}${infer R}`
? H extends C
? `${DropChar<R, C>}`
: `${H}${DropChar<R, C>}`
: ''

MinusOne
- ParseIntの処理で
RemoveLeadingZeros
をしないとParseInt<'099'> => number型
になる
type NumberLiteral = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9'
type MinusOneMap = {
'0': '9';
"1": "0";
"2": "1";
"3": "2";
"4": "3";
"5": "4";
"6": "5";
"7": "6";
"8": "7";
"9": "8";
}
type ReverseString<S extends string> =
S extends `${infer F}${infer R}`
? `${ReverseString<R>}${F}`
: S
type RemoveLeadingZeros<S extends string> = S extends '0'
? S
: S extends `${'0'}${infer R}`
? RemoveLeadingZeros<R>
: S
type Initial<T extends string> =
ReverseString<T> extends `${infer F}${infer R}`
? ReverseString<R>
: T
type MinusOneForString<S extends string> =
S extends `${Initial<S>}${infer Last extends NumberLiteral}`
? Last extends '0'
? `${MinusOneForString<Initial<S>>}${MinusOneMap[Last]}`
: `${Initial<S>}${MinusOneMap[Last]}`
: ''
type ParseInt<T extends string> =
RemoveLeadingZeros<T> extends `${infer Digit extends number}` ? Digit : never
type MinusOne<T extends number> = T extends 0
? -1
: ParseInt<MinusOneForString<`${T}`>>

PickByType
type PickByType<T, U> = {
[P in keyof T as T[P] extends U ? P : never]: T[P]
}

IsUnion
-
T extends never
の挙動の理解が重要Link
type IsUnion<T, C=T> = [T] extends [never]
? false
: T extends never
? false
: [C] extends [T]
? false
: true
補足
Unionの分配を止めるテクニック
'a' extends 'a' | 'b' | 'c' | 'd' => true
['a'] extends ['a' | 'b' | 'c' | 'd' ] => false

StartsWith
- extendsに
${U}${infer R}
を指定するのがポイント。
type StartsWith<T extends string, U extends string> = T extends `${U}${infer R}` ? true : false

EndWith
- StartsWithと同じ方法.
type EndsWith<T extends string, U extends string> = T extends `${infer H}${U}` ? true : false

PartialByKeys
- 交差型を作成してMergeするのが重要.
type Merge<T> = {
[P in keyof T]: T[P]
}
type PartialByKeys<T, K extends keyof T = keyof T> = Merge<{
[P in keyof Omit<T, K>]: T[P]
} & {
[P in K]?: T[P]
}>

RequiredByKey
- プロパティに
-?
をつけると、プロパティが必須になる - Required<T> はTypeScriptのビルトインパッケージで定義されている
type Merge<T> = {
[P in keyof T]: T[P]
}
type RequiredByKeys<T, K extends keyof T = keyof T> = Merge<{
[P in keyof Omit<T, K>]?: T[P]
} & {
[P in K]-?: T[P]
}>

Mutable
-
-readonly
をつけると readonlyを外すことができる. -
T extends (Record<string, unknown> | readonly unknown[])>
で型を制限する
type Mutable<T extends (Record<string, unknown> | readonly unknown[])> = {
-readonly [P in keyof T]: T[P]
}

OmitByType
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P]
}

ObjectEntries
回答1
- Object形式で処理して、最後に Obj[keyof Obj] でUnion型にする方法 解説
- オプションのキーは、すべて必須キーに変換する必要がある.
HandleUndefined
を使用する.- 下記の①と②を場合分けする必要がある。Optionalプロパティの場合とそうでない場合を
T[P] extends Required<T>[P]
で判定する. (参考)
① {name?: string} => [name: string]
② {name: string | undefined} => [name: string | undefined]
- 下記の①と②を場合分けする必要がある。Optionalプロパティの場合とそうでない場合を
- Partial型は型にundefinedを付加するため、その処理をする必要がある.
type HandleUndefined<T, P extends keyof T> = T[P] extends Required<T>[P]
? T[P]
: T[P] extends infer F | undefined
? F
: never
type ObjectEntries<T> = {
[P in keyof T]-?: [P, HandleUndefined<T, P>]
}[keyof T]
回答2
-
Distributive Conditional Types
を使用してUnion型にしてから処理をする方法
type ObjectEntries<T, K extends keyof T = keyof T> = K extends keyof T
? [K, [T[K]] extends [undefined] ? T[K] : Exclude<T[K], undefined>]
: never

Shift
- 配列の要素を extends でマッピングする.
type Shift<T extends any[]> = T extends [infer H, ...infer R]
? R
: T

TupleToNestedObject
- 配列を extends でマッピングする.
-
Record<H, TupleToNestedObject<R, U>>
で再起処理する.
type TupleToNestedObject<T extends any[], U> = T extends [infer H, ...infer R]
? H extends string
? Record<H, TupleToNestedObject<R, U>>
: never
: U
type TupleToNestedObject<T extends unknown[], U> = T extends [infer H, ...infer R]
? {[K in H&string]: TupleToNestedObject<R, U>}
: U

OmitByType
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P]
}

Tuple to NestedObject
type TupleToNestedObject<T, U> = T extends [infer F, ...infer R]
? F extends string
? Record<F, TupleToNestedObject<R, U>>
: never
: U
type TupleToNestedObject<T, U> = T extends [infer F, ...infer R]
? F extends string
? Record<F, TupleToNestedObject<R, U>>
: never
: U

Reverse
type Reverse<T extends any[]> = T extends [...infer F, infer R]
? [R, ...Reverse<F>]
: []

Flip Arguments
-
- 関数の引数の型をtuple型にして返す型
-
- TypeScriptで関数の返り値を型に変換する
type Reverse<T extends any[]> = T extends [infer H, ...infer R] ? [...Reverse<R>, H] : []
type FlipArguments<T extends (...args: any[]) => any> = (...args: Reverse<Parameters<T>>) => ReturnType<T>

BEM
type BEM<B extends string, E extends string[], M extends string[]> = M extends []
? `${B}__${E[number]}`
: E extends []
? `${B}--${M[number]}`
: `${B}__${E[number]}--${M[number]}`

InorderTraversal
interface TreeNode {
val: number
left: TreeNode | null
right: TreeNode | null
}
type InorderTraversal<T extends TreeNode | null> = [T] extends [TreeNode]
? [...InorderTraversal<T['left']>, T['val'], ...InorderTraversal<T['right']>]
: [];

Flip
-
T extends Record<string, any>
しないとis not assignable to type
になる.
type Flip<T extends Record<string, any>> = {
[P in keyof T as `${T[P]}`]: P
}

IsTuple
- T['length']が存在するかでタプル型 or 配列型を判別する
-
number extends T['length']
で確認する. -
T['length']
とnumber
を逆にするとタプル型と配列型を区別でいなくなる.T['length'] extends number
はダメ❌
type LengthType<T extends readonly unknown[]> = T['length']
type T1 = LengthType<number[]> // number型
type T2 = LengthType<[number]> // 1 のUnion型
- タプル型
type IsTuple<T> = [T] extends [never]
? false
: T extends readonly unknown[]
? number extends T['length']
? false
: true
: false

Zip
type Zip<T extends unknown[], U extends unknown[]> = T extends [infer HT, ...infer RT]
? U extends [infer HU, ...infer RU]
? [[HT, HU], ...Zip<RT, RU>]
: []
: []
type Zip<
T extends unknown[],
U extends unknown[]
> =
[T, U] extends [
[infer THead, ...infer TTail],
[infer UHead, ...infer UTail]
]
? [[THead, UHead], ...Zip<TTail, UTail>]
: [];

TrimRight
-
type whiteSpace = ' ' | '\n' | '\t'
が宣言することが重要
type whiteSpace = ' ' | '\n' | '\t'
type TrimRight<S extends string> = S extends `${infer W}${whiteSpace}`
? TrimRight<W>
: S

Without
- number型 と number[]型 があることに注意.
type Without<T extends unknown[], U extends number | number[]> = T extends [infer H, ...infer R]
? H extends (
U extends number[]
? U[number]
: U
)
? [...Without<R, U>]
: [H, ...Without<R, U>]
: []

Trunc
type Trunc<N extends number | string> = `${N}` extends `${infer H}.${infer R}`
? H extends ''
? '0'
: H
: `${N}`

IndexOf
-
F extends U
とU extends F
で比較すること.
type IndexOf<T extends any[], U, I extends number[] = []> = T extends [infer F, ...infer R]
? (F extends U
? U extends F
? true
: false
: false) extends true
? I['length']
: IndexOf<R, U, [...I, 1]>
: -1

Join
type Join<T extends string[], U extends string | number> = T['length'] extends 1
? T[0]
: T extends [infer F extends string, ...infer R extends string[]]
? `${F}${U}${Join<R, U>}`
: ''

LastIndexOf
type SimpleEqual<T, U> = T extends U
? U extends T
? true
: false
: false
type LastIndex<T extends any[]> = T extends [any, ...infer R] ? R['length'] : -1
type Last<T extends any[]> = T[LastIndex<T>]
type LastIndexOf<T extends any[], U> = T extends []
? -1
: (
SimpleEqual<U, Last<T>> extends true
? LastIndex<T>
: T extends [...infer R, any]
? LastIndexOf<R, U>
:-1
)

ConstructTuple
type ConstructTuple<L extends number, R extends unknown[] = []> = R['length'] extends L
? R
: ConstructTuple<L, [...R, unknown]>

NumberRange
- neverはArray -> Tuple型にすると削除される.
type NumberRange<Start extends number, End extends number, R extends number[] = []> = R['length'] extends End
? [...R, R['length']][number]
: R['length'] extends Start
? NumberRange<[...R, 1]['length'] extends number ? [...R, 1]['length'] : never, End, [...R, R['length']]>
: NumberRange<Start, End, [...R, never]>

Subsequence
type Subsequence<T extends any[]> = T extends [infer F, ...infer R] ? ([F] | [F, ...Subsequence<R>] | Subsequence<R>) : T;

Combination
type Combination<T extends string[], U = T[number], K = U> = K extends string
? K | `${K} ${Combination<[], Exclude<U, K>>}`
: ''

Subsequence
type Subsequence<T extends any[]> = T extends [infer Left, ...infer Rest]
? [Left] | [Left, ...Subsequence<Rest>] | Subsequence<Rest>
: T

CheclRepeatedChars
-
R extends
${string}${H}${string}
が出てくるかが大事.
type CheckRepeatedChars<T extends string, R = string> = T extends `${infer H}${infer R}`
? R extends `${string}${H}${string}`
? true
: CheckRepeatedChars<R>
: false

StrToUnion
- uniqの判定をUnionで行う.
type StrToUnion<S extends string> =
S extends `${infer F}${infer Rest}`
? F | StrToUnion<Rest>
: never
type FirstUniqueCharIndex<T extends string, P extends string = '', C extends any[] = []> =
T extends `${infer F}${infer Rest}`
? F extends StrToUnion<`${P}${Rest}`>
? FirstUniqueCharIndex<Rest, `${P}${F}`, [...C, 1]>
: C['length']
: -1

GetMiddleElement
- 中間の要素を特定する方法.
-
T extends [infer H, ...infer R]
とT extends [...infer H, any]
を組み合わせる. - 配列の長さが
0
と1
を判定する.
-
type GetMiddleElement<T extends any[]> = T extends [infer H1, ...infer R1]
? R1 extends [...infer H2, any]
? H2['length'] extends 0
? T
: H2['length'] extends 1
? H2
: GetMiddleElement<H2>
: [H1]
: []

FindEles
- 配列 -> Union型にする方法
Arr[number]
type FindEles<T extends any[], Acc extends any[] = [], Result extends any[] = []> = T extends [infer F, ...infer R]
? F extends R[number] | Acc[number]
? FindEles<R, [...Acc, F], [...Result]>
: FindEles<R, [...Acc, F], [...Result, F]>
: Result

Concat
回答1
type Concat<T extends any[], U extends any[]> = [...T, ...U]
回答2
type Concat<T extends any[], U extends any[], Result extends any[] = []> = T['length'] extends 0
? U['length'] extends 0
? Result
: U extends [infer UH, ...infer UR]
? Concat<T, UR, [...Result, UH]>
: Result
: T extends [infer TH, ...infer TR]
? Concat<TR, U, [...Result, TH]>
: Concat<[], U, Result>

DeepMutable
type DeepMutable<T> = {
-readonly [P in keyof T]: T[P] extends Function
? T[P]
: T[P] extends { [x: string | number | symbol]: any }
? DeepMutable<T[P]>
: T[P]
}

All
type All<A extends any[], T> =
A extends (infer I)[] ?
Equal<I, T>
: never
type All<T extends any[], S> = (T[number] extends S ? true : false) extends true
? Equal<T[number], S>
: false

Filter
- 1つずつ取り出して比較する.
type Filter<T extends unknown[], P> = T extends [infer H, ...infer R]
? [...(H extends P ? [H] : []), ...Filter<R, P>]
: []

Zen
type ModifierKeys = ['cmd', 'ctrl', 'opt', 'fn']
type Helper<T> = T extends [infer F extends string, ...infer R extends string[]]
? `${F} ${R[number]}` | Helper<R>
: never
type Combs = Helper<ModifierKeys>

SimpleEqual
- SimpleEqualを別のtypeにしておくと仕様が変わったときに対応できる.
type SimpleEqual<T, S> = T extends S
? true
: false
type ReplaceFirst<T extends readonly unknown[], S, R, Cnt extends any[] = []> = T extends [infer H, ...infer Rest]
? SimpleEqual<H, S> extends true
? Cnt['length'] extends 0
? [R, ...ReplaceFirst<Rest, S, R, [...Cnt, 1]>]
: [H, ...ReplaceFirst<Rest, S, R, Cnt>]
: [H, ...ReplaceFirst<Rest, S, R, Cnt>]
: []

SimpleVue
- thisの表し方.
- methodsは自分自身にもアクセスできる、その定義方法。
Implement a simpiled version of a Vue-like typing support.
By providing a function name SimpleVue (similar to Vue.extend or defineComponent), it should properly infer the this type inside computed and methods.
In this challenge, we assume that SimpleVue take an Object with data, computed and methods fields as it's only argument,
data is a simple function that returns an object that exposes the context this, but you won't be accessible to other computed values or methods.
computed is an Object of functions that take the context as this, doing some calculation and returns the result. The computed results should be exposed to the context as the plain return values instead of functions.
methods is an Object of functions that take the context as this as well. Methods can access the fields exposed by data, computed as well as other methods. The different between computed is that methods exposed as functions as-is.
The type of SimpleVue's return value can be arbitrary.
declare function SimpleVue<
A extends Record<string, unknown>,
B extends Record<string, unknown>,
C extends Record<string, unknown>
> (options: {
data: (this: never) => A
computed: {
[P in keyof B]: (this: A, ...args: unknown[]) => B[P]
}
methods: {
[P in keyof C]: (this: A & B & { [K in keyof C]: (...args: unknown[]) => C[K]}) => C[P]
}
}): any

Currying
- 引数を
extends [infer H, ...infer R]
で比較して、引数があるかぎりCurryを再帰する.
type Curry<P extends readonly any[], R> = P extends []
? R
: P extends [infer H, ...infer T]
? (arg: H) => Curry<T, R>
: R
declare function Currying<P extends readonly any[], R extends boolean>(
fn: (...args: P) => R
): P extends [] ? () => R : Curry<P, R>

UnionToIntersection
type UnionToIntersection<U> = (
U extends U ? (x: U) => unknown : never
) extends (x: infer R) => unknown
? R
: never

GetRequired
- Requiredの使い方.
Required<T>[P]
type GetRequired<T extends object> = {
[P in keyof T as T[P] extends Required<T>[P] ? P : never]: T[P]
}

Get Optional
- GetRequiredの判定条件を逆にする.
type GetOptional<T extends object> = {
[P in keyof T as T[P] extends Required<T>[P] ? never : P]: T[P]
}

RequiredKeys
- Requiredのプロパティのみのオブジェクトを作り、そこから
keyof
でプロパティ名を取得する.
type RequiredKeys<T extends object> = keyof {
[P in keyof T as T[P] extends Required<T>[P] ? P : never]: T[P]
}

OptionalKeys
- RequiredKeysの判定条件を逆にする.
type OptionalKeys<T extends object> = keyof {
[P in keyof T as T[P] extends Required<T>[P] ? never : P]: T[P]
}

CapitalizeWords
-
Capitalize<StringType>
- 先頭の文字を大文字にする型
-
英数字以外をマッチする方法
Uppercase<S> extends Lowercase<S>
type CapitalizeWords<S extends string, W extends string = ''> = S extends `${infer H}${infer R}`
? Uppercase<H> extends Lowercase<H>
? `${CapitalizeWords<W>}${H}${CapitalizeWords<R>}`
: CapitalizeWords<R, `${W}${H}`>
: Capitalize<W>

Camelcase
- Prevを用意して1つ前の型を保存しておくことが大事.
type CamelCase<S extends string = '', Prev extends string = ''> = S extends `${infer H}${infer R}`
? Uppercase<H> extends Lowercase<H>
? `${Prev}${CamelCase<R, H>}`
: Prev extends '_'
? `${CamelCase<R, Uppercase<H>>}`
: `${Prev}${CamelCase<R, Lowercase<H>>}`
: Prev

IsAny
- anyは何でもマッチするので
0 extends 1 & T
にすることが大事.
type IsAny<T> = 0 extends 1 & T ? true : false

ToNumber
-
S extends ${infer N extends number}
すると明示的にnumber型にできる.
type ToNumber<S extends string> = S extends `${infer N extends number}`
? N
: never

FilterOut
type FilterOut<T extends any[], F> = T extends [infer H, ...infer R]
? [H] extends [F]
? FilterOut<R, F>
: [H, ...FilterOut<R, F>]
: []

Enum
- 今回、配列からオブジェクトにしたい。
- そのために、
{ [K in keyof obj]: obj[K]}
の形を作りたい。 - 配列もオブジェクトなので mapped type が使用できる.
-
mapped type は可能だが、配列の要素以外に
length
やPrototype
のforEach
なども取得してしまう。
const arr = ['a', 'b', 'c'] as const
type Arr = typeof arr
type ArrToObj = {
[K in keyof Arr]: Arr[K]
}
解決策は2つ。
1つ目は、number型のみ抽出する方法.
type ArrToObj = {
[K in keyof Arr as K extends `${number}` ? K : never]: Arr[K]
}
2つ目は、T型を使用する方法.
mapped typeには型変数Tの型が配列だった場合に、全てのプロパティをマップするのではなく要素の型のみをマップしてくれる機構がある。
type GetArrKey<T> = {[P in keyof T]: string};
type Key = GetArrKey<Arr> // readonly [string, string, string]になる
回答
type Enum<T extends readonly string[], N extends boolean = false> = {
readonly [P in keyof T as P extends `${number}` ? Capitalize<T[P]> : never]:
N extends false ? T[P] : P extends `${infer I extends number}` ? I : never
}

Union to Tuple
前提知識
関数間で交差演算子演算を行い、「input」を使って交差結果を持つ関数の戻り値を取り出すと、次のように交差演算を行った最後の関数の戻り値が得られる
type Intersepted = (() => 'a') & (() => 'b') & (() => 'c')
type Last = Intersepted extends () => infer R ? R : never // 'c'
// @jo32:
// "
// the magic of this solution is
// given a type like `(arg: U) => any | (arg: V) => any`,
// if you want to find an argument (using keyword infer)
// satisfying U and V, the type of it must be `U & V`
// "
下記のUを満たすものはUnion型.
// Notes
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#type-inference-in-conditional-types
type Foo<T> =
T extends {
a: infer U;
b: infer U;
}
? U
: never;
type T10 = Foo<{ a: string; b: string }>; // string
type T11 = Foo<{ a: string; b: number }>; // string | number
下記の infer U
を満たすもの.
関数の変数は共変であるためそれを満たすものは intersection
型.
type Bar<T> =
T extends {
a: (x: infer U) => void;
b: (x: infer U) => void;
}
? U
: never;
type T20 = Bar<{
a: (x: string) => void;
b: (x: string) => void;
}>; // string
type T21 = Bar<{
a: (x: string) => void;
b: (x: number) => void;
}>; // string & number
// `string & number` equals `never`.
回答
type UnionToTuple<T, Result extends unknown[] = [], Last = GetUnionLast<T>> = [
T
] extends [never]
? Result
: UnionToTuple<Exclude<T, Last>, [Last, ...Result]>
type GetUnionLast<T> = UnionToIntersectionFn<T> extends () => infer R
? R
: never
type UnionToIntersectionFn<T> = (
T extends T ? (x: () => T) => unknown : never
) extends (x: infer R) => unknown
? R
: never

join
type JoinRes<P extends string[], D extends string> = P extends [infer H, ...infer R]
? R extends []
? `${P[0]}`
: `${P[0]}${D}${JoinRes<R, D>}`
: ''
declare function join<D extends string>(delimiter: D): <P extends string[]>(...parts: P) => JoinRes<P, D>

Pinia
type GettersReturnType<G> = {
readonly [P in keyof G]: G[P] extends (...args: any[]) => infer R ? R : never
}
interface Store<S, G, A> {
id: string
state: () => S
getters?: G & ThisType<Readonly<S> & GettersReturnType<G> & A>
actions?: A & ThisType<S & GettersReturnType<G> & A>
}
declare function defineStore<S, G, A>(store: Store<S, G, A>): Readonly<S> & GettersReturnType<G> & A

Camelize
type SnakeToCamel<S> = S extends `${infer H}_${infer H1}${infer R}`
? `${H}${Uppercase<H1>}${SnakeToCamel<R>}`
: S
type CamelizeArray<T> = T extends [infer H, ...infer R]
? [Camelize<H>, ...CamelizeArray<R>]
: []
type Camelize<T> = {
[P in keyof T as SnakeToCamel<P>]: T[P] extends unknown[]
? CamelizeArray<T[P]>
: T[P] extends object
? Camelize<T[P]>
: T[P]
}

DropString
type Union<R extends string> = R extends `${infer F}${infer R}`
? F | Union<R>
: never
type DropString<S, R extends string, Result extends string = ''> = S extends `${infer H}${infer Rest}`
? H extends Union<R>
? DropString<Rest, R, Result>
: DropString<Rest, R, `${Result}${H}`>
: Result

Split
type Split<S extends string, SEP extends string, Result extends string[] = []> = string extends S
? string[]
: SEP extends ''
? S extends `${infer H}${infer Rest}`
? Split<Rest, SEP, [...Result, H]>
: Result
: S extends `${infer H}${SEP}${infer Rest}`
? Split<Rest, SEP, [...Result, H]>
: [...Result, S]

ClassPublicKeys
type ClassPublicKeys<A> = keyof A

IsRequiredKey
optionalプロパティとrequireプロパティの差分をどのように表現するかがポイント。
ポイント1
extends した時の挙動。
optionalプロパティはあってもなくても良いため、extendsすると下記のような挙動になる。
type T = { a: number, b?: string }
type T1 = Omit<T, 'a'> extends T ? true : false; // false
type T2 = Omit<T, 'b'> extends T ? true : false; // true
回答
ポイント1を使ってrequireプロパティのキーを取得する型を作成してみる.
type RequireKeys<T> = keyof {
[P in keyof T as Omit<T, P> extends T ? never : P]: T[P]
}
次にRequireKeysと渡されたKeyを比較してみる
type RequireKeys<T> = keyof {
[P in keyof T as Omit<T, P> extends T ? never : P]: T[P]
}
type IsRequiredKey<T, K extends keyof T> = K extends RequireKeys<T> ? true : false
1つテストが通らない。
Union型が渡された場合、状況によっては trueだが、違う状況では false みたいなことが起こる。
その場合、 true or falseではなく boolean型になる.
なので IsRequiredKey
全体が true と等しいかを判定する.
type RequireKeys<T> = keyof {
[P in keyof T as Omit<T, P> extends T ? never : P]: T[P]
}
type IsRequiredKey<T, K extends keyof T> = (K extends RequireKeys<T> ? true : false) extends true ? true : false
回答(スマート)
type IsRequiredKey<T, K extends keyof T> = undefined extends T[K] ? false : true

ObjectFromEntries
ポイント1
UnionをforEachで回してオブジェクトを作成する
type ObjectFromEntries<T extends [key: string, value: unknown]> = {
[P in T[0]]: P
}
ポイント2
Union型から指定されたものを取得する方法はExtractを使用する
Extractの使い方
回答
type ObjectFromEntries<T extends [key: string, value: unknown]> = {
[P in T[0]]: Extract<T, [P, unknown]>[1]
}

Function argument inference only handles one overload
Basically TS is only looking at the last element of the intersection.

IsPalindrome
ポイント1
①. 最初の文字と最後の文字を比較する
②. 1の結果がTrueなら、最初と最後の文字を削除して①に戻る
最初の文字を取得するtype
type GetHeadStr<S extends string> = S extends `${infer H}${infer R}`
? H
: S
最後の文字を取得する関数
文字の並び順を逆にソートしてから、一番最初の文字を返すようにすると一番最後の文字を取得できる.
type GetLastStr<S extends string> = GetHeadStr<SortReverse<S>>
type SortReverse<S extends string, Result extends string = ''> = S extends `${infer H}${infer R}`
? SortReverse<R, `${H}${Result}`>
: Result
回答
まずは①を実装
type IsPalindrome<T extends string | number> = GetHeadStr<`${T}`> extends GetLastStr<`${T}`>
? true
: false
次に②を実装
type GetHeadStr<S extends string> = S extends `${infer H}${infer R}`
? H
: S
type GetLastStr<S extends string> = GetHeadStr<SortReverse<S>>
type SortReverse<S extends string, Result extends string = ''> = S extends `${infer H}${infer R}`
? SortReverse<R, `${H}${Result}`>
: Result
type RemoveHeadAndLastString<S extends string> = S extends `${GetHeadStr<S>}${infer R}${GetLastStr<S>}`
? R
: ''
type IsPalindrome<T extends string | number, S extends string = `${T}`> = S extends `${infer H}${infer R}`
? GetHeadStr<S> extends GetLastStr<S>
? IsPalindrome<RemoveHeadAndLastString<S>>
: false
: true

MutableKeys
ポイント1
Readonly<T>
ポイント2
Pick<T, Keys>
回答
Pick<T, P>
と Readonly<Pick<T, P>>
を比較して、false
なら Mutable になる.
type MutableKeys<T> = keyof {
[P in keyof T as
Equal<
Pick<T, P>, // can be Copy
Readonly<Pick<T, P>>
> extends true
? never
: P
]: never;
};

Intersection
ポイント1
[[1, 2], [2, 3], [2, 2]]
から (1 | 2) & (2 | 3) & (2 | 2)
を作成する.
配列を Union型 [1, 2]
-> (1 | 2)
にするには T[number] にする.
回答
type Intersection<T extends unknown[]> = T extends [infer H, ...infer R]
? (H extends unknown[] ? H[number] : H) & Intersection<R>
: unknown

BinaryToDecimal
ポイント1
typeで数を導き出すには 配列(Arr['length']
)を使う.
ポイント2
2の累乗を数える方法.
数字が0 の時は 2^2 => [...Result, ...Result]
数字が1 の時は 2^2 + 1 => [...Result, ...Result, 1]
回答
type BinaryToDecimal<S extends string, Result extends unknown[] = []> = S extends `${infer H}${infer R}`
? H extends '0'
? BinaryToDecimal<R, [...Result, ...Result]>
: BinaryToDecimal<R, [...Result, ...Result, 1]>
: Result['length']

ObjectKeyPath
回答
type GenNode<K extends string | number, Root extends boolean> = Root extends true
? `${K}`
: `.${K}` | (K extends number ? `[${K}]` | `.[${K}]` : never)
type ObjectKeyPath<
T extends object,
Root extends boolean = true,
K extends keyof T = keyof T
> = K extends string | number
? GenNode<K, Root>
| (T[K] extends object
? `${GenNode<K, Root>}${ObjectKeyPath<T[K], false>}`
: never)
: never

TwoSum
ポイント1
typeで数を導き出すには 配列( Array['length']
)を使う.
数値(number)から配列を作る型 を作ると 足し算(Sum)する型も作れる
type LengthToArray<T extends number, R extends unknown[] = []> = R['length'] extends T
? R
: LengthToArray<T, [...R, unknown]>
type Sum<A extends number, B extends number> = A extends number
? B extends number
? [...LengthToArray<A>, ...LengthToArray<B>]['length']
: never
: never
ポイント2
数値の組み合わせを作る方法.
-
配列の先頭の数
と先頭以外の数値
を取得する - 先頭 と それ以外の数値(
Array[number]
)を使用する. - それ以外の数値だけで1から繰り返す.
下記は渡された配列から全ての組み合わせのUnion型を作る型.
type ToArray<T extends number, U> = U extends any ? [U, T] : never
type Combination<T extends number[] = []> = T extends [infer H extends number, ...infer R extends number[]]
? ToArray<H, R[number]> | Combination<R>
: never
type T1 = Combination<[1, 2, 3]>
// [2, 1] | [3, 1] | [3, 2]
回答
type TwoSum<T extends number[], U extends number> = T extends [infer H extends number, ...infer R extends number[]]
? U extends Sum<H, R[number]>
? true
: TwoSum<R, U>
: false

ValidDate
ポイント1
① 「2月」=> 28日まで.
②「4月,6月,9月,11月」=> 30日まで.
③「1月,3月,5月,7月,8月,10月,12月」=> 31日まで.
回答
type ValidMonth<S extends string> = S extends '01' | '02' | '03' | '04' | '05' | '06' | '07' | '08' | '09' | '10' | '11' | '12'
? true
: false
type ValidDay<D1 extends string, D2 extends string, Month extends string> = `${D1}${D2}` extends '00'
? false
: D1 extends '0' | '1'
? true
: Month extends '02'
? `${D1}${D2}` extends '21' | '22' | '23' | '24' | '25' | '26' | '27' | '28'
? true
: false
: Month extends '04' | '06' | '09' | '11'
? `${D1}${D2}` extends '21' | '22' | '23' | '24' | '25' | '26' | '27' | '28' | '29' | '30'
? true
: false
: Month extends '01' | '03' | '05' | '07' | '08' | '12'
? `${D1}${D2}` extends '21' | '22' | '23' | '24' | '25' | '26' | '27' | '28' | '29' | '30' | '31'
? true
: false
: false
type ValidDate<T extends string> = T extends `${infer M1}${infer M2}${infer D1}${infer D2}`
? ValidMonth<`${M1}${M2}`> extends true
? ValidDay<D1, D2, `${M1}${M2}`> extends true
? true
: false
: false
: false

Merge
ポイント1
Object.assignは同じプロパティ名があったら、後から追加する方が優先される.
Omit<A, keyof B> & B
をすると AからBと同じプロパティ名が削除された後に A と B のintersection型になる.
回答
type Merge<T> = {
[P in keyof T]: T[P]
}
type Assign<
T extends Record<string, unknown>,
U extends any[]
> = U extends [infer H, ...infer R]
? H extends Record<string, unknown>
? Assign<Omit<T, keyof H> & H, R>
: Assign<T, R>
: Merge<T>

GreaterThan
ポイント1
大きさの比較方法.
数を扱うときは Array['length']
を使用.
GreaterThan<A, B, Increment extends unknown[] = []>
で Increamentの配列要素を1つずつ足していって A と B を比較する.
type GreaterThan<A extends number, B extends number, R extends unknown[] = []> = A extends B
? false
: R['length'] extends B
? true
: R['length'] extends A
? false
: GreaterThan<A, B, [...R, unknown]>
回答
type GreaterThan<A extends number, B extends number, R extends unknown[] = []> = A extends B
? false
: R['length'] extends B
? true
: R['length'] extends A
? false
: GreaterThan<A, B, [...R, unknown]>
type Maximum<T extends number[], Max extends number = never> =
T extends [infer H extends number, ...infer R extends number[] ]
? Maximum<R, [Max] extends [never]
? H
: GreaterThan<Max, H> extends true
? Max
: H>
: Max

CapitalizeNestObjectKeys
ポイント1
Key名をCapitalizeする関数を作成する.
type ConvertKeys<S extends string = ''> = S extends `${infer H}${infer R}`
? `${Uppercase<H>}${R}`
: S
ポイント2
as
を使用して objectのKey名をCapitalizeする.
type CapitalizeNestObjectKeys<T> = {
[P in keyof T as ConvertKeys<P>]: T[P]
}
回答
type ConvertKeys<S extends string = ''> = S extends `${infer H}${infer R}`
? `${Uppercase<H>}${R}`
: S
type CapitalizeNestObjectKeys<T> = T extends [infer H, ...infer R]
? [CapitalizeNestObjectKeys<H>, ...CapitalizeNestObjectKeys<R>]
: T extends Record<any, unknown>
? {
[P in keyof T as ConvertKeys<P>]: T[P] extends object
? CapitalizeNestObjectKeys<T[P]>
: T[P]
}
: T

FizzBuzz
ポイント1
下記の3つの型を作成する
- 整数が3で割り切れる場合は true を返すtpye
- 整数が3で割り切れる場合は true を返すtpye
- 整数が3 と 5で割り切れる場合は true を返すtpye
下準備
- GratherThan ->数値A > 数値B の場合 true を返す
- IsSame -> 数値A と 数値B が等しい場合 true を返す
type GratherThan<A extends number, B extends number, Increment extends unknown[] = []> = A extends B
? false
: B extends Increment['length']
? true
: A extends Increment['length']
? false
: GratherThan<A, B, [...Increment, unknown]>
type IsSame<A extends number, B extends number> = A extends B ? true : false
整数が3で割り切れる場合は true を返すtpye
IsDiv3<T, [...Increment, unknown, unknown, unknown]>
のように unknown
を3つ使い3の倍数を表現している.
type IsDiv3<T extends number, Increment extends unknown[] = []> = IsSame<T, Increment['length']> extends true
? true
: GratherThan<Increment['length'], T> extends true
? false
: IsDiv3<T, [...Increment, unknown, unknown, unknown]>
整数が5で割り切れる場合は true を返すtpye
IsDiv5<T, [...Increment, unknown, unknown, unknown, unknown, unknown]>
のように unknown
を5つ使い5の倍数を表現している.
type IsDiv5<T extends number, Increment extends unknown[] = []> = IsSame<T, Increment['length']> extends true
? true
: GratherThan<Increment['length'], T> extends true
? false
: IsDiv5<T, [...Increment, unknown, unknown, unknown, unknown, unknown]>
整数が3 と 5で割り切れる場合は true を返すtpye
type IsDiv3_5<T extends number> = IsDiv3<T> extends true
? IsDiv5<T> extends true
? true
: false
: false
回答
type GratherThan<A extends number, B extends number, Increment extends unknown[] = []> = A extends B
? false
: B extends Increment['length']
? true
: A extends Increment['length']
? false
: GratherThan<A, B, [...Increment, unknown]>
type IsSame<A extends number, B extends number> = A extends B ? true : false
type IsFizz<T extends number, Increment extends unknown[] = []> = IsSame<T, Increment['length']> extends true
? true
: GratherThan<Increment['length'], T> extends true
? false
: IsFizz<T, [...Increment, unknown, unknown, unknown]>
type IsBuzz<T extends number, Increment extends unknown[] = []> = IsSame<T, Increment['length']> extends true
? true
: GratherThan<Increment['length'], T> extends true
? false
: IsBuzz<T, [...Increment, unknown, unknown, unknown, unknown, unknown]>
type IsFizzBuzz<T extends number> = IsFizz<T> extends true
? IsBuzz<T> extends true
? true
: false
: false
type FizzBuzz<N extends number, Result extends unknown[] = [], NN extends unknown[] = [...Result, unknown]> = IsSame<Result['length'], N> extends true
? Result
: IsFizzBuzz<NN['length']> extends true
? FizzBuzz<N, [...Result, 'FizzBuzz']>
: IsFizz<NN['length']> extends true
? FizzBuzz<N, [...Result, 'Fizz']>
: IsBuzz<NN['length']> extends true
? FizzBuzz<N, [...Result, 'Buzz']>
: FizzBuzz<N, [...Result, `${NN['length']}`]>

RepeatStr
ポイント1
機能を分けて考えます.
まず3つの型を作成.
- 文字を数える型. (先頭から連続して同じ文字が続いている文字数)
- 文字を削除する型. (先頭から連続している文字を削除して文字を返す)
- 連続した文字を作成する型.
1. 文字を数える型
type CountStr<T extends string, S extends string, Cnt extends unknown[] = []> = T extends `${S}${infer R}`
? CountStr<R, S, [...Cnt, unknown]>
: Cnt['length'] extends 1
? ''
: Cnt['length']
2. 文字を削除する型
type RemoveStr<T extends string, S extends string> = T extends `${S}${infer R}`
? RemoveStr<R, S>
: T
3. 連続した文字を作成する型
type RepeatStr<T extends string, Repeat extends number, Count extends unknown[] = [], Result extends string = ''> = Count['length'] extends Repeat
? Result
: RepeatStr<T, Repeat, [...Count, unknown], `${Result}${T}`>
回答
namespace RLE {
export type Encode<S extends string, Result extends string = ''> = S extends `${infer H}${infer R}`
? Encode<RemoveStr<S, H>, `${Result}${CountStr<S, H>}${H}`>
: Result
export type Decode<S extends string, Result extends string = ''> = S extends `${infer Num extends number}${infer Str extends string}${infer R}`
? Decode<R, `${Result}${RepeatStr<Str, Num>}`>
: S extends `${infer Str extends string}${infer R}`
? Decode<R, `${Result}${RepeatStr<Str, 1>}`>
: Result
}

Path
ポイント1
object型 から objectのUnion型を取得する方法.
type Val<T extends Record<PropertyKey, unknown>> = T[keyof T]
type T1 = Val<typeof example2>
declare const example2: {
bar: {
a: string
}
baz: {
b: number
c: number
}
}
回答
type Path<T> = T extends Record<PropertyKey, unknown>
? {
[P in keyof T]: [P, ...Path<T[P]>] | [P];
}[keyof T]
: never;

SnakeCase<T>
ポイント1
記号(!$%などの英数字以外)を判定する方法.
記号は小文字も大文字も同じになる性質を利用する.
type IsSymbolString<T extends string> = Uppercase<T> extends Lowercase<T>
? true
: false
ポイント2
大文字かどうか判定する方法.
まずは記号かどうかを判定して、その後Uppercase<T>
を使用して判定する.
type IsUppserString<T extends string> = IsSymbolString<T> extends true
? false
: T extends Uppercase<T>
? true
: false
回答
type IsSymbolString<T extends string> = Uppercase<T> extends Lowercase<T>
? true
: false
type IsUppserString<T extends string> = IsSymbolString<T> extends true
? false
: T extends Uppercase<T>
? true
: false
type SnakeCase<T extends string> = T extends `${infer H}${infer R}`
? IsUppserString<H> extends true
? `_${Lowercase<H>}${SnakeCase<R>}`
: `${H}${SnakeCase<R>}`
: ''
回答2
もじ文字列の中に記号がないことが保証されているならこの方法でも良さそう.
type SnakeCase<T> = T extends `${infer H}${infer R}`
? Uppercase<H> extends H
? `_${Lowercase<H>}${SnakeCase<R>}`
: `${H}${SnakeCase<R>}`
: '';

IsNegativeNumber<T>
ポイント1
下記のように数字を文字列にして Template Literal Types -${infer X}
と比較するだけでは2つでエラーになる。
type IsNegativeNumber<T extends number> = `${T}` extends `-${infer S}`
? true
: false
1つ目はNumberの場合.
2つ目はUnionの場合.
ポイント2
Numberの判定方法.
条件式に型引数 T
を持ってくること.
条件式の箇所に numberを持ってくると型引数の値が1 or 2 or 3などの数値でも trueになる.
例: 2 extends number ? true : false // true
IsNumberType<T> = number extends T
? true
: false
ポイント3
Unionの判定.
- union distribution(分配)
- タプル化 or 配列化
1. union distribution(分配)
type IsUnion<T> = T extends any ? true : false
// IsUnion<'a' | 'b' | 'c'>
// => ('a' extends any ? true : false) | ('b' extends any ? true : false) | ('c' extends any ? true : false)
2. タプル化 or 配列化
タプル化しない場合.
'a' | 'b' | 'c' extends 'a' ? true : false
になると「union distribution(分配)」が起きて boolean になる.
'a' | 'b' | 'c' extends 'a' ? true : false
// => ('a' extends 'a' ? true : false) | ('b' extends 'a' ? true : false) | ('c' extends 'a' ? true : false)
// => true | false | false
// => boolean
分配させないためにはタプル化する.
['a' | 'b' | 'c'] extends ['a'] ? true : false
これを元にIsUnionを作成する.
type IsUnion<T, U = T> = T extends U
? [U] extends [T]
? false
: true
: never
// IsUnion<'a' | 'b' | 'c'>
// => (['a' | 'b' | 'c'] extends ['a'] ? true : false) | (['a' | 'b' | 'c'] extends ['b'] ? true : false) | (['a' | 'b' | 'c'] extends ['c'] ? true : false)
回答
type IsUnion<T, U = T> = T extends any
? [U] extends [T]
? false
: true
: never
type IsNumberType<T> = number extends T ? true : false
type IsNegativeNumber<T extends number> = IsNumberType<T> extends true
? never
: IsUnion<T> extends true
? never
: `${T}` extends `-${infer S}`
? true
: false

TrimLeft
ポイント1
文字列の1文字目と2文字目以降にマッチさせる方法
S extends `${infer H}${infer R}`
回答
type TrimLeft<S extends string> = S extends `${infer H}${infer R}`
? H extends ' ' | '\n' | '\t'
? TrimLeft<R>
: S
: S

Trim
ポイント1
TrimLeft と TrimRightの条件をUnionで繋げる。
type WhiteSpace = ' ' | '\t' | '\s';
type TrimLeft<S extends string> = S extends `${WhiteSpace}${infer R}`
? TrimLeft<R>
: S;
type WhiteSpace = ' ' | '\t' | '\s';
type TrimRight<S extends string> = S extends `${infer R}${WhiteSpace}`
? TrimRight<R>
: S;
回答
type WhiteSpace = ' ' | '\t' | '\s';
type Trim<S extends string> = S extends `${infer R}${WhiteSpace}` | `${WhiteSpace}${infer R}`
? Trim<R>
: S;

Capitalize
ポイント1
TrimLeft と TrimRightの条件をUnionで繋げる。
type WhiteSpace = ' ' | '\t' | '\s';
type TrimLeft<S extends string> = S extends `${WhiteSpace}${infer R}`
? TrimLeft<R>
: S;
type WhiteSpace = ' ' | '\t' | '\s';
type TrimRight<S extends string> = S extends `${infer R}${WhiteSpace}`
? TrimRight<R>
: S;
回答
type WhiteSpace = ' ' | '\t' | '\s';
type Trim<S extends string> = S extends `${infer R}${WhiteSpace}` | `${WhiteSpace}${infer R}`
? Trim<R>
: S;

OptionalUndefined
問題
OptionalUndefined<T,Props>というutil型を実装して、未定義にできるTのすべてのプロパティをオプショナル・プロパティに変える。さらに、変更可能なプロパティを制限するために、2番目の-optional-ジェネリックPropsを渡すことができます。
OptionalUndefined<{ value: string | undefined, description: string }>
// { value?: string | undefined; description: string }
OptionalUndefined<{ value: string | undefined, description: string | undefined, author: string | undefined }, 'description' | 'author'>
// { value: string | undefined; description?: string | undefined, author?: string | undefined }
回答
type OptionalUndefined<T, P extends keyof T = keyof T> = Omit<
Omit<T, P> & {
[K in P as undefined extends T[K] ? K : never]?: T[K];
} & {
[K in P as undefined extends T[K] ? never : K]: T[K];
},
never
>;
まとめ
下記3つの交差型を作成する。
Omit<T, P>
- Pを除外した型を作成
{ [K in P as undefined extends T[K] ? K : never]?: T[K]; }
- PのプロパティでValueにundefinedが含まれているパターンの型を作成
{ [K in P as undefined extends T[K] ? never : K]: T[K];}
- PのプロパティでValueにundefinedが含まれていないパターンの型を作成

Tree path array
問題
配列の形式でツリーの可能なパスを検証するPath型を作成する。
declare const example: {
foo: {
bar: {
a: string;
};
baz: {
b: number
c: number
}
};
}
// Possible solutions:
// []
// ['foo']
// ['foo', 'bar']
// ['foo', 'bar', 'a']
// ['foo', 'baz']
// ['foo', 'baz', 'b']
// ['foo', 'baz', 'c']
回答
type Path<T> = T extends Record<PropertyKey, unknown>
? {
[P in keyof T]: [P, ...Path<T[P]>] | [P];
}[keyof T]
: never;
まとめ
Path<typeof example['foo']['baz']> => ['b'] | ['c']
を満たす型を作成してみる
type Path<T> = T extends Record<PropertyKey, unknown>
? {
[P in keyof T]: [P];
}
: never;
declare const example: {
foo: {
bar: {
a: string
}
baz: {
b: number
c: number
}
}
}
上記のままだとオブジェクトになる。
type A = Path<typeof example['foo']['bar']>
// {
// b: ["b"];
//. c: ["c"];
// }
オブジェクトをUnion型にするために [keyof T]
を追加する。
type Path<T> = T extends Record<PropertyKey, unknown>
? {
[P in keyof T]: [P];
}[keyof T]
: never;
次に Path<typeof example['foo']> => ['bar'] | ['baz'] | ['bar', 'a'] | ['baz', 'b'] | ['baz', 'c']
を考えてみる。
| ['bar', 'a'] | ['baz', 'b'] | ['baz', 'c']
が前回からプラスになっている。ここは再帰を使って実装する。
type Path<T> = T extends Record<PropertyKey, unknown>
? {
[P in keyof T]: [P] | [P, ...Path<T[P], P>]
}[keyof T]
: never;

Binary to Decimal
問題
0と1からなる正確な文字列型Sを受け取り、Sを2進数とみなしたときにSに対応する正確な数値型を返すBinaryToDecimal<S>を実装する。Sの長さは8以下であり、Sは空ではないと仮定できる。
type Res1 = BinaryToDecimal<'10'>; // expected to be 2
type Res2 = BinaryToDecimal<'0011'>; // expected to be 3
回答
type BinaryToDecimal< S extends string, Result extends 1[] = []> =
S extends `${infer H}${infer R}`
? H extends '0'
? BinaryToDecimal<R, [...Result, ...Result]>
: BinaryToDecimal<R, [...Result, ...Result, 1]>
: Result['length']
まとめ
- 2進数なため、下記のようにできることがポイント。
- 0の時:
BinaryToDecimal<R, [...Result, ...Result]>
- 1の時:
BinaryToDecimal<R, [...Result, ...Result, 1]>
- 0の時:

Run-length encoding
問題
AAABCCXXXXXXYのような文字列シーケンスが与えられる。ランレングス符号化された文字列3AB2C6XYを返す。また、その文字列のデコーダを作る。
回答
// 文字(S)と探す文字(Search)を渡して、文字Sの先頭からSearchが何文字続くかを調べて回数を返す型
// 例: HeadStrCount<'aaabcdef', 'a'> => 3
type HeadStrCount<S extends string, Search extends string, Result extends 1[] = []> =
S extends `${infer H}${infer R}`
? H extends Search
? HeadStrCount<R, Search, [...Result, 1]>
: Result['length']
: Result['length']
// 文字(S)と数値(N)を渡して、文字SをN回反復して返す型
// 例: Repeat<'a', 5> => 'aaaaa'
type Repeat<S extends string, Count extends number, N extends 1[] = [], Result extends string = ''> =
N['length'] extends Count
? Result
: Repeat<S, Count, [...N, 1], `${Result}${S}`>
// 文字(S)と数値(N)を渡して、Sの先頭からN文字削除して返す型
// 例: Slice<'abcde', 2> => 'cde'
type Slice<S extends string, Len extends number, N extends 1[] = []> =
S extends `${infer _}${infer R}`
? N['length'] extends Len
? S
: Slice<R, Len, [...N, 1]>
: ''
namespace RLE {
export type Encode<S extends string, Result extends string = ''> = S extends `${infer H}${infer _}`
? RLE.Encode<Slice<S, HeadStrCount<S, H>>, `${Result}${HeadStrCount<S, H> extends 1 ? '' : HeadStrCount<S, H>}${H}`>
: Result
export type Decode<S extends string> =
S extends `${infer N extends number}${infer S}${infer R}`
? `${Repeat<S, N>}${RLE.Decode<R>}`
: S extends `${infer H extends string}${infer R}`
? `${H}${RLE.Decode<R>}`
: ''
}
まとめ
Encode
- 先頭の文字列から調べていく
-
S extends
{infer _}``{infer H}
- 先頭の文字が何文字続くか調べる
HeadStrCount<S, H>
-
先頭の文字列 x 2の結果
を作成する
Slice<S, HeadStrCount<S, H>
- 3の結果を再帰で処理する
RLE.Encode<Slice<S, HeadStrCount<S, H>>, `${Result}${HeadStrCount<S, H> extends 1 ? '' : HeadStrCount<S, H>}${H}`>
Decode
「文字の前に数値があるパターン2...
」と「文字の前に数値がないパターンa...
(aの前に1が省略)」を分けて処理すると良い。
- 「文字の前に数値があるパターン
2...
」-
S extends
{infer S}${infer R}`{infer N extends number}
-
- 「文字の前に数値がないパターン
a...
(aの前に1が省略)」${Repeat<S, N>}${RLE.Decode<R>}
補足
ランレングス符号化とは、主に画像データの圧縮に用いられる符号化方式の一種で、連続する同一の値を「色×回数」という列の長さ(run-length)の情報に置き換える方式のことである。参考