Open134

TypeChallenge-extra

katsuo55katsuo55

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にしている。

katsuo55katsuo55

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つに分けて考える。
    1. ParseQueryString: クエリの文字列から1組のKVを取得する
    • 例: Key1=Value1&Key2=Value2&...から Key1=Value1を取得
    1. ParseKey: 1組のKV文字列からKeyValueを取得
    • 例: Key1=Value1 から Key1Value1 を取得
    1. AddProperty: KeyValueを結果に追加する

補足

PropertyKey

TypeScriptの組み込み型の1つであり、文字列またはシンボル型を表す型の合併型。文字列型はオブジェクトのプロパティ名として使われ、シンボル型はシンボルという一意の識別子を表す。
PropertyKey は JavaScript オブジェクトのプロパティ名として有効なすべての値を表します。

katsuo55katsuo55

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>>;

まとめ

  1. 数値を配列に変換する。マイナスの値の場合、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]>
  1. 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>を実行する。

    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]> => []
    1. SliceRec<[2, 3, 4, 5, 6, 7, 8, 9, 10], [any], [any, any, any, any]> => []
    1. SliceRec<[3, 4, 5, 6, 7, 8, 9, 10], [], [any, any, any]> => [3]
    1. SliceRec<[4, 5, 6, 7, 8, 9, 10], [], [any, any]> => [3, 4]
    1. SliceRec<[5, 6, 7, 8, 9, 10], [], [any]> => [3, 4, 5]
    1. SliceRec<[6, 7, 8, 9, 10], [], []> => [3, 4, 5]
katsuo55katsuo55

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つに分けて考える。
  1. Compares two digits: 2つの数を比較する。 ※数は1桁。
  2. CompareByDigits: 2つの数を比較する。 ※2つの数は同じ桁数。
  • 先頭の数を取得して同じ数かどうかを確認。同じ数の場合、2番目以降の数でCompareByDigits<Aの2番目以降の数, Bの2番目以降の数>
  • 同じではない場合、Compares<Aの先頭の数, Bの先頭の数>
  1. CompareByLength: 2つの数の桁数を比較する
  2. ComparePositives: 2つの正数を比較する
  • まず、同じ桁数か確認するCompareByLength
  • 同じ桁数の場合、CompareByDigitsで数値比較
  • 同じ桁数ではない場合、CompareByLengthの結果をそのまま返す。(桁数の比較結果がそのまま数値比較の結果)
  1. Comparator: 2つの数を正の数 or 負の数かを判定して、ComparePositivesをコールする

補足

CompareDigits

BよりAが先にある場合Lowerになる。逆の場合はGreaterになる。

'0123456789' extends `${string}${A}${string}${B}${string}`
katsuo55katsuo55

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>の理解が難しいため中間処理を書きました。

  1. Curry<[string, number, boolean], Result, []>
  2. ((...args: [string]) =>Curry<[string], Result>) & Curry<[number, boolean], [], [string]>
  3. ((...args: [string]) =>Curry<[string], Result>) & ((...args: [string, number]) =>Curry<[number], Result>) & Curry<[boolean], [], [string, number]>
  4. ((...args: [string]) =>Curry<[string], Result>) & ((...args: [string, number]) =>Curry<[number], Result>) & ((...args: [string, number, boolean]) => Result)
katsuo55katsuo55

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<{A}`>, Reverse<`{B}>>>
  • NumberToTuple: 数値を配列に変換する
  • Reverse: 文字列を逆にする
  • ToNumber: 文字列を数値に変換する
  • CarryOver: 渡された数値が 10以上かどうか確認する
  • LastDigit: 渡された数値の先頭を除いた数を返す。例) LastDigit<12345> => 2345
  • Add: 加算する。数値 => 配列 => 数値 にして計算する。

補足

ToNumberがない場合、下記のエラーが出る。[1, 2, 3]['Length'] は数値になるとは限らない。

katsuo55katsuo55

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)と実装すると再帰の数が減る。
katsuo55katsuo55

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

まとめ

  1. unique symbolを使ってunique型を宣言します。
    declare const uq: unique symbol
    declare const dummy: unique symbol: uq

  2. type UU<T> = Exclude<T, undefined>: UU
    Tからundefinedを除外します。これは、undefinedでない型を取得するための型エイリアスです。

  3. type GetTags<B> = ...
    型Bのタグを抽出します。この型は再帰的に呼び出され、B内のすべてのタグを配列として取得します。

  4. type TagRec<B, T extends string> = ...
    型Bにタグを追加または置き換えます。Tは新しいタグの名前を指定します。

  5. type Tag<B, T extends string> = ...: Tag
    型Bにタグを追加します。Tは新しいタグの名前を指定します。この型はさまざまな条件に基づいて異なるタグを付けます。

  6. type Member<T extends any[], U> = ...
    型Uが型Tの要素であるかどうかを判定します。

  7. type PrefixUnion<T> = ...
    型T内のすべての可能なプレフィックスを抽出します。

  8. type Subseq<T, U> = ...
    型Tが型Uのサブシーケンス(一部分として含まれるか)であるかどうかを判定します。

9 type UnTag<B> = ...
型Bからタグを削除します。

  1. type HasTag<B, T extends string> = ...: HasTag
    型Bが指定されたタグTを持つかどうかを判定します。

  2. type HasTags<B, T extends readonly string[]> = ...
    型Bが指定された一連のタグを持つかどうかを判定します。

  3. type HasExactTags<B, T extends readonly string[]> = ...
    型Bが指定された一連のタグを正確に持つかどうかを判定します。

katsuo55katsuo55

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の比較がなくなり動作が軽くなる。
katsuo55katsuo55

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回の再起でいいように工夫している.
katsuo55katsuo55

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] }

まとめ

  1. DistributeUnions:与えられた型 T を処理します。もし T が配列型(unknown[])の場合、 DistributeArray を呼び出して配列内の要素を処理し、オブジェクト型の場合は DistributeObject を呼び出してオブジェクト内のプロパティを処理します。それ以外の型の場合は T をそのまま返します。

  2. DistributeArray:共用型内の配列を処理します。最初の要素(H)を DistributeUnions で処理し、残りの要素(T)に再帰的に DistributeArray を適用します。展開された配列は、新しい共用型として返されます。

  3. 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']
  1. 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判定する機能。分散しないようにするため[]を使用する。

  1. ObjHelper:これは DistributeObject の内部で使用され、オブジェクトのプロパティを処理します。プロパティ名(K)とプロパティの値(V)を使用して新しいオブジェクトを生成します。

  2. Merge:展開されたオブジェクトを単一のオブジェクトにマージします。

katsuo55katsuo55

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']
katsuo55katsuo55

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']

まとめ

  1. HashMap:アルファベット文字をキー(a-z)とし、1から26の数値とするオブジェクト型です。アルファベットをキーとして対応する数字を取得するために使用されます。

  2. ToArray :指定された長さ N の配列を生成します。再帰的に [[], ...Result] を Result 配列に追加することで、指定された長さまでの配列を生成します。

  3. Hash :文字列 S を受け取り、各文字を HashMap から対応する数値に変換して配列 A に追加します。文字列 S を分解して数値の配列を生成します。最後は数値(配列の長さ)を返します。
    例1) HashMap<'a'> => 1
    例2) HashMap<'b'> => 2
    例3) HashMap<'c'> => 3
    例4) HashMap<'abc'> => 6(1 + 2 + 3)

  4. assertArrayIndex:配列 A とキー K を受け取り、配列 A が特定の条件を満たすかどうかを検証します。条件は、number extends A['length'] というもので、配列 A の長さが number 型である場合に検証が行われます。検証が成功すると、特定の型が返されます。

  5. Index:配列型 Array の 'key' プロパティにアクセスすることで、ハッシュマップのキーに対応する数値を取得します。

katsuo55katsuo55

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」に場合分けして地道に文字列比較で処理する。
特にこれといって特殊なテクニックはないです。

katsuo55katsuo55

typeof

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

katsuo55katsuo55

BigInt と Numberの違い

大きすぎて number プリミティブで表すことができない数を、表現したり操作したりするために使用する。

公式ドキュメント

Numberと異なる点

  1. 組み込みの Math オブジェクト内のメソッドでは利用できない
  2. 演算で Number の値と混ぜることができない(同じ型に統一する必要がある)
  3. BigInt を Number へ変換する際には精度が落ちることがある

比較演算

  1. BigInt 値は Number 値と厳密等価ではありませんが、等価にはなる。
0n === 0
// ↪ false

0n == 0
// ↪ true
  1. 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
katsuo55katsuo55

Omit<T, K>

備考

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

型アサーション 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;

型アサーションとキャストの違い

キャストとは、実行時にある値の型を別の型に変換することです。
型アサーションは、実行時に影響しません。値の型変換はしない。あくまでコンパイル時にコンパイラーに型を伝えるだけ。

katsuo55katsuo55

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>;
katsuo55katsuo55

DeepReadonly<T>

TypeChallengeの問題を解く。

type DeepReadonly<T> = {
  readonly [P in keyof T]: keyof T[P] extends never ? T[P] : DeepReadonly<T[P]>
}
katsuo55katsuo55

Tuple to Union

type TupleToUnion<T extends any[]> = T[number]
katsuo55katsuo55

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
}
katsuo55katsuo55

Last of Array

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

Pop

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

Promise.all

問題

関連技術

answer
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がネストしたケース
Promiseがネストしたケース

katsuo55katsuo55

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];
katsuo55katsuo55

Distributive Conditional Types

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

参考

katsuo55katsuo55

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プラパティの挙動は異なる

katsuo55katsuo55

Flatten

T extends [infer H, ...infer R] を使うのが肝。
あとはひたすら展開。

type Flatten<T> = T extends []
  ? [] 
  : T extends [infer H, ...infer R]
    ? [...Flatten<H>, ...Flatten<R>]
    : [T]
katsuo55katsuo55

Append to object

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

Absolute

数値は文字列に変換して extends するのが重要。

type Absolute<T extends number | string | bigint> = `${T}` extends `-${infer R}` ? R : `${T}`
katsuo55katsuo55

String to Union

1文字ずつ取り出すのが重要。

type StringToUnion<T extends string> = T extends `${infer H}${infer R}` ? H | StringToUnion<R> : never
katsuo55katsuo55

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
katsuo55katsuo55

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
}
katsuo55katsuo55

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
katsuo55katsuo55

IsNever

この知識を知っているかを聞かれている。

type IsNever<T> = [T] extends [never] ? true : false
katsuo55katsuo55

neverについて

  • never は共用体ではない。空集合。
katsuo55katsuo55

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)
    }
  }

参考

ブルーベリー本

katsuo55katsuo55

any extends never の挙動

anyはstringやvoidを継承している状態と継承していない状態の両方を併せ持っている。
neverはそもそも判定すらされず、全体がneverと評価されてしまう。

この挙動を利用して、例えばanyを判定する型を作ることができる。

// neverを継承するかの判定
// この型は、Tがanyのときtrue|false, neverのときnever, それ以外のときfalseになる
type ExtendsNever<T> = T extends never ? true : false;

参考
参考2

katsuo55katsuo55

Replace Key

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

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]
}
katsuo55katsuo55

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>]

回答2
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>]
katsuo55katsuo55

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>}`
  : ''
katsuo55katsuo55

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}`>>
katsuo55katsuo55

PickByType

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

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
katsuo55katsuo55

StartsWith

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

EndWith

type EndsWith<T extends string, U extends string> = T extends `${infer H}${U}` ? true : false
katsuo55katsuo55

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]
}>
katsuo55katsuo55

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]
}>
katsuo55katsuo55

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]
}
katsuo55katsuo55

OmitByType

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

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]
  • Partial型は型にundefinedを付加するため、その処理をする必要がある.
回答1
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
katsuo55katsuo55

Shift

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

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
回答2
type TupleToNestedObject<T extends unknown[], U> = T extends [infer H, ...infer R]
  ? {[K in H&string]: TupleToNestedObject<R, U>}
  : U
katsuo55katsuo55

OmitByType

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

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
katsuo55katsuo55

Reverse

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

Flip Arguments

  • Parameters

    • 関数の引数の型をtuple型にして返す型
  • ReturnType

    • 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>
katsuo55katsuo55

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]}`
katsuo55katsuo55

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']>]
  : [];
katsuo55katsuo55

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
}
katsuo55katsuo55

IsTuple

  • T['length']が存在するかでタプル型 or 配列型を判別する
  • number extends T['length'] で確認する.
  • T['length']number を逆にするとタプル型と配列型を区別でいなくなる. T['length'] extends number はダメ❌
タプル型と配列型 lengthの違い
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
katsuo55katsuo55

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>]
    : []
  : []
beautiful code
type Zip<
  T extends unknown[],
  U extends unknown[]
> =
  [T, U] extends [
    [infer THead, ...infer TTail],
    [infer UHead, ...infer UTail]
  ]
  ? [[THead, UHead], ...Zip<TTail, UTail>]
  : [];
katsuo55katsuo55

TrimRight

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

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>]
  : []
katsuo55katsuo55

Trunc

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

IndexOf

  • F extends UU 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
katsuo55katsuo55

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>}`
    : ''
katsuo55katsuo55

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
  ) 
katsuo55katsuo55

ConstructTuple

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

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]>
katsuo55katsuo55

Subsequence

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

Combination


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

Subsequence

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

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
katsuo55katsuo55

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
katsuo55katsuo55

GetMiddleElement

  • 中間の要素を特定する方法.
    • T extends [infer H, ...infer R]T extends [...infer H, any] を組み合わせる.
    • 配列の長さが 01 を判定する.
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]
  : []
katsuo55katsuo55

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
katsuo55katsuo55

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>
katsuo55katsuo55

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]
}
katsuo55katsuo55

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
katsuo55katsuo55

Filter

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

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>
katsuo55katsuo55

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>]
  : []
katsuo55katsuo55

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
katsuo55katsuo55

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>
katsuo55katsuo55

UnionToIntersection

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

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]
}
katsuo55katsuo55

Get Optional

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

RequiredKeys

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

OptionalKeys

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

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>
katsuo55katsuo55

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
katsuo55katsuo55

IsAny

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

ToNumber

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

FilterOut

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

Enum

  • 今回、配列からオブジェクトにしたい。
  • そのために、 { [K in keyof obj]: obj[K]} の形を作りたい。
  • 配列もオブジェクトなので mapped type が使用できる.
  • mapped type は可能だが、配列の要素以外に lengthPrototypeforEach なども取得してしまう。
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
}
katsuo55katsuo55

Union to Tuple

前提知識

関数間で交差演算子演算を行い、「input」を使って交差結果を持つ関数の戻り値を取り出すと、次のように交差演算を行った最後の関数の戻り値が得られる

type Intersepted = (() => 'a') & (() => 'b') & (() => 'c')
type Last = Intersepted extends () => infer R ? R : never // 'c'

https://github.com/type-challenges/type-challenges/issues/25061

// @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
katsuo55katsuo55

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>

katsuo55katsuo55

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
katsuo55katsuo55

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]
}
katsuo55katsuo55

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
katsuo55katsuo55

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]
katsuo55katsuo55

ClassPublicKeys

type ClassPublicKeys<A> = keyof A
katsuo55katsuo55

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
katsuo55katsuo55

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]
}
katsuo55katsuo55

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
katsuo55katsuo55

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;
};
katsuo55katsuo55

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
katsuo55katsuo55

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']
katsuo55katsuo55

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
katsuo55katsuo55

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

数値の組み合わせを作る方法.

  1. 配列の先頭の数先頭以外の数値 を取得する
  2. 先頭 と それ以外の数値( Array[number] )を使用する.
  3. それ以外の数値だけで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
katsuo55katsuo55

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
katsuo55katsuo55

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>
katsuo55katsuo55

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
katsuo55katsuo55

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
katsuo55katsuo55

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']}`]>
katsuo55katsuo55

RepeatStr

ポイント1

機能を分けて考えます.

まず3つの型を作成.

  1. 文字を数える型. (先頭から連続して同じ文字が続いている文字数)
  2. 文字を削除する型. (先頭から連続している文字を削除して文字を返す)
  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
}
katsuo55katsuo55

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;
katsuo55katsuo55

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>}`
  : '';
katsuo55katsuo55

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の判定.

  1. union distribution(分配)
  2. タプル化 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
katsuo55katsuo55

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
katsuo55katsuo55

Trim

問題

ポイント1

TrimLeft と TrimRightの条件をUnionで繋げる。

TrimLeft
type WhiteSpace = ' ' | '\t' | '\s';
type TrimLeft<S extends string> = S extends `${WhiteSpace}${infer R}`
  ? TrimLeft<R>
  : S;
TrimRight
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;
katsuo55katsuo55

Capitalize

問題

ポイント1

TrimLeft と TrimRightの条件をUnionで繋げる。

TrimLeft
type WhiteSpace = ' ' | '\t' | '\s';
type TrimLeft<S extends string> = S extends `${WhiteSpace}${infer R}`
  ? TrimLeft<R>
  : S;
TrimRight
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;
katsuo55katsuo55

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つの交差型を作成する。

  1. Omit<T, P>
  • Pを除外した型を作成
  1. { [K in P as undefined extends T[K] ? K : never]?: T[K]; }
  • PのプロパティでValueにundefinedが含まれているパターンの型を作成
  1. { [K in P as undefined extends T[K] ? never : K]: T[K];}
  • PのプロパティでValueにundefinedが含まれていないパターンの型を作成
katsuo55katsuo55

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;
katsuo55katsuo55

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]>
katsuo55katsuo55

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

  1. 先頭の文字列から調べていく
  • S extends {infer H}{infer _}``
  1. 先頭の文字が何文字続くか調べる
  • HeadStrCount<S, H>
  1. 先頭の文字列 x 2の結果を作成する
  • Slice<S, HeadStrCount<S, H>
  1. 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 N extends number}{infer S}${infer R}`
  • 「文字の前に数値がないパターンa...(aの前に1が省略)」
    • ${Repeat<S, N>}${RLE.Decode<R>}

補足

ランレングス符号化とは、主に画像データの圧縮に用いられる符号化方式の一種で、連続する同一の値を「色×回数」という列の長さ(run-length)の情報に置き換える方式のことである。参考