Open82

type challenges 【中級編】

yutake27yutake27

ReturnType

type MyReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer U ? U : never

MyReturnType<T extends (...args: any[]) => any>だけ書ければ後はそのまま

yutake27yutake27

Omit

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

初めas kが抜けてて

[k in keyof T extends K ? never : k]: T[k]

と書いていてType parameter 'k' has a circular constraint.というエラーが発生
kがvalueなのに型として使ってしまっていたのでas kを追加

yutake27yutake27

Readonly2

最初デフォルト値の指定が分からず調べた

type MyReadonly2<T, K extends keyof T | null = null> = K extends keyof T ? Readonly<Pick<T, K>> & Omit<T, K> : Readonly<T>

こんなやつを書いたが通らず。(実装的には多分正しいが型チェック関数的に通らなかった)

無駄に初期値をnullにする必要はなく、keyof Tで良かった

type MyReadonly2<T, K extends keyof T = keyof T> = Readonly<Pick<T, K>> & Omit<T, K>
yutake27yutake27

DeepReadonly

type isObject<T> = T extends () => void ? false : T extends object ? true : false
type DeepReadonly<T> = {
  readonly [P in keyof T]: isObject<T[P]> extends true ? DeepReadonly<T[P]> : T[P]
}

オブジェクトのみという制約だったので、こんなんでいけた。

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

最初シンプルにextends objectとしていたが、関数もobjectとして判定されてしまってたので関数の場合はfalseになるisObjectを作成

yutake27yutake27

TupleToUnion

問題

タプルをユニオンに変換

type Arr = ['1', '2', '3']

type Test = TupleToUnion<Arr> // expected to be '1' | '2' | '3'

解答

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

メモ

配列からユニオンを生成するテンプレを覚えていたのでいけた

const arr = [1, 2, 3] as const
type hoge = typeof arr[number]
yutake27yutake27

Chainable Options

問題

チェインを作成

try

難しかった。。

最初こんな感じで書いた

type Chainable<T extends object = {}> = {
  option(key: string, value: any): Chainable<{key: typeof value} & T>
  get(): T
}

これだとkey, valueが型として扱われず、

{ key: any } & { key: any} & {key: any}

のようになってしまった。

key, valueを型として扱うという発想が出てこず、諦めた。

解答

type Chainable<T extends object = {}> = {
  option<K extends string, V extends any>(key: K, value: V): Chainable<Record<K, V> & Omit<T, K>>
  get(): T
}

途中までは良さげだった。

key, valueを型として扱うために関数の先頭にジェネリクスを付ける必要があった。(この発想がなかった)

勉強になった。Recordも覚えておく。

yutake27yutake27

これだとエラー発生箇所を考慮できていなかった。

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

が正しい

yutake27yutake27

Last of Array

配列の最後のtypeを取得

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

これで通った。

解答見たらもっと短くいけたみたい。

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

スプレッド構文で最後の要素とそれ以外も取得可能。これを使えば簡潔に書ける

yutake27yutake27

Pop

配列の最後の要素を除いた配列を取得

Last of Arrayとほぼ一緒だったので一瞬だった

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

Promise.all

type AwaitedAll<T extends readonly any[]> = T extends readonly [infer U, ...infer V] ? [Awaited<U>, ...AwaitedAll<V>] : []
declare function PromiseAll<T extends readonly any[]>(values: T): Promise<AwaitedAll<T>>

こんなものを思いついたが、テストケース4つのうちas constを使っていない2つで弾かれた。

原因はPromiseAll<T extends readonly any[]>とTの型をreadonlyにしてしまっていたこと。ここではTはreadonlyにする必要はなく、valuesだけreadonlyにする必要があった。

valuesだけreadonlyにする場合以下のように書くことができる

type AwaitedAll<T extends any[]> = T extends [infer U, ...infer V] ? [Awaited<U>, ...AwaitedAll<V>] : T
declare function PromiseAll<T extends any[]>(values: readonly [...T]): Promise<AwaitedAll<T>>

ただ、これでも最後のテストケースで失敗する。

yutake27yutake27
declare function PromiseAll<T extends any[]>(values: readonly [...T]):
  Promise<{ [K in keyof T]: T[K] extends Promise<infer R> ? R : T[K] }>;

これでいけるみたい。

readonly [...T]という表現に馴染みがない。(上級編でも見かけなかった気がする)

yutake27yutake27

LookUp

最初↓で良い感じに分配されていけるのではないかと思ったがダメだった

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

次にUnionをinferで分解しようと思って↓を書いたら通った。

type LookUp<U extends {type: string}, T extends string> = U extends infer V | infer W ? V extends {type: string} ? V['type'] extends T ? V : never : never : never

結構無駄なことをしている気がして解答を見たら最初の案の方向性でいけたらしい。ただし自分の最初の書き方だとダメみたい。

type LookUp<U, T> = U extends {type: T} ? U : never

extends {type: T}がミソ。

yutake27yutake27

Trim left

文字列の左端の空白文字を削除する

最初どうすれば良いか分からず、試しに文字列でもinfer使えるんじゃね?というノリで使ってみたところ${}で囲めば使えた。

type TrimLeft<S extends string> = S extends ` ${infer U}` ? TrimLeft<U> : S

また、スペースだけでなく\n,\tもエスケープする必要があった。
三項演算子で各エスケープ条件を繋げていけばいけそう!→でも3回三項演算子使うの面倒...→試しにunion使っていけないかな?→いけた

type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t' }${infer U}` ? TrimLeft<U> : S

だいぶ成長を感じた。

yutake27yutake27

Trim

TrimLeftとほぼ一緒。両端から空白文字を除外する。

TrimLeft, TrimRightを定義してやればいけると考え、それでいけた。

type TrimLeft<S extends string> = S extends `${' ' | '\n' | '\t'}${infer T}` ? TrimLeft<T> : S
type TrimRight<S extends string> = S extends `${infer T}${' ' | '\n' | '\t'}` ? TrimRight<T> : S
type Trim<S extends string> = TrimLeft<TrimRight<S>>

ただ、ちょっと冗長である。
もっと簡潔に書いている人がいた。

type space = " " | "\n" | "\t"
type Trim<S extends string> = S extends `${space}${infer T}` | `${infer T}${space}` ? Trim<T> : S;

こちらの方が簡潔で良い。extendsの後にUnionを持ってくる発想はなかった。

yutake27yutake27

Capitalize

文字の1文字目を大文字化するやつ。
知らなかったがユーティリティ型に存在しているらしい。

初めはtypescriptで大文字化とかできるの??って感じだったがとりあえず空文字('')だけ通るようにしようと思って↓を書いてみた。

type MyCapitalize<S extends string> = S extends `${infer T}${infer U}` ? hoge : S

無事空文字は通った。

次にtypescriptで大文字化するような機能はないか調べてみた。
そしたらCapitalizeというユーティリティ型の他にUppercaseというユーティリティ型も存在することを発見した。
https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html
https://zenn.dev/uhyo/articles/typescript-intrinsic

試しに使ってみたところ、通った。

type MyCapitalize<S extends string> = S extends `${infer T}${infer U}` ? `${Uppercase<T>}${U}` : S

Uppercase使っちゃって良いのかは疑問だったが、他の解答見てたところ使っていたので大丈夫みたい。

yutake27yutake27

Replace

文字列Sに含まれる文字FromをToに一度だけ置き換える。

一度だけで良いのがポイント。

inferを使えば簡単にできる。infer使うと空文字も該当してしまうので空文字は除外する必要があった。

type Replace<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer T}${From}${infer U}` ? `${T}${To}${U}` : S
yutake27yutake27

ReplaceAll

文字列Sに含まれる文字列Fromを全てToに置き換える

Replaceを再帰的に行えばいける

type ReplaceAll<S extends string, From extends string, To extends string, prev extends string = ''> = From extends '' ? S : S extends `${prev}${infer T}${From}${infer U}` ? ReplaceAll<`${prev}${T}${To}${U}`, From, To, `${prev}${T}${To}`> : S

↑のように実装した。

なんか無駄に頑張っている気がして解答見たらもっと良いのがあった。

type ReplaceAll<S extends string, From extends string, To extends string> = From extends '' ? S : S extends `${infer T}${From}${infer U}` ? `${T}${To}${ReplaceAll<U, From, To>}` : S

ReplaceAllを文字列中で使うのがポイント。

yutake27yutake27

Append Argument

与えられた関数型 Fn と任意の型 A に対して、第一引数に Fn を取り、第二引数に A を取り、Fn の引数に A を追加した関数型 G を生成

関数をinferするのどうやってやるか忘れていた。ReturnTypeやった時の記録を見て思い出した。

↓の感じでinferできる

Fn extends (...args: infer T) => infer U

↑をもとに素直に書いたらいけた。

type AppendArgument<Fn extends (...args: any[]) => any, A> = Fn extends (...args: infer T) => infer U ? (...args: [...T, A]) => U : never
yutake27yutake27

Permutation

Union 型を Union 型の値の順列を含む配列に変換
↓のような変換をする

  type perm = Permutation<'A' | 'B' | 'C'>; // ['A', 'B', 'C'] | ['A', 'C', 'B'] | ['B', 'A', 'C'] | ['B', 'C', 'A'] | ['C', 'A', 'B'] | ['C', 'B', 'A']

解答

かなり難しかった。

問題をパッと見た時まずUnion distributionをうまく使うんだろうな〜と思った。

type Permutation<T>があったとき、Tが Unionだったら要素を一つ取り出したものと残りの要素を再帰的にPermutateしたもののリストを返し、Unionじゃなかったら[T]を返すみたいにすればいけるのでは?と考えた。

TがUnionであることってどうやって判定するんだ??となったが、たまたまこの前Unionの判定をやっている記事を読んでいたのを思い出した。
https://zenn.dev/pokotyan/articles/fd47f277ed80c0#union-distribution

この記事の内容がまさにドンピシャで、この記事をもとに↓のような解答を作成した。

type Permutation<T, U = T> = T extends U ? [U] extends [T] ? [T] : [T, ...Permutation<Exclude<U, T>>] : never

ポイントはunion distributionが発生するのは型変数に union を渡して、かつその union が条件分岐に利用される場合だということ。

この場合Tは分配されるがUは分配されずunionのままである。

上記解答でテストケース4つ通った。

neverを渡された時だけ通っていなかった。
never無視すりゃええんか。簡単やな。これでいけるやろ↓

type Permutation<T, U = T> = T extends never ? [] : T extends U ? [U] extends [T] ? [T] : [T, ...Permutation<Exclude<U, T>>] : never

これだといけなかった、、、
Permutation<never>がどうしてもneverになってしまった。

type A = never extends never ? true : false

type A = trueになるのに何で??って感じだったが調べたらそういう性質らしい。
neverは空のunionとして扱われるよう。

https://zenn.dev/kj455/articles/fb8e08b6a455a0#never-型

T[] extends never[][T] extends [never]と書くと良いみたい。

最終的に↓のコードで前テストケース通った。めでたしめでたし。めっちゃ勉強になった。

type Permutation<T, U = T> = T[] extends never[] ? [] : T extends U ? [U] extends [T] ? [T] : [T, ...Permutation<Exclude<U, T>>] : never

追記

後から解答見たがPermutation<never> = []になっているので↓でいけるみたい

type Permutation<T, U = T> = T[] extends never[] ? [] : T extends any ? [T, ...Permutation<Exclude<U, T>>] : never
yutake27yutake27

Length of string

文字列の長さを計算する

解答

配列の長さがArray["length"]でとれることを覚えていたので文字列を配列に変換→変換し終わったら長さを計算でいけると思った

この方針をそのまま実装してみたら通った

type LengthOfString<S extends string, Past extends string[] = []> = S extends "" ? Past['length'] : S extends `${infer Head}${infer Rest}` ? LengthOfString<Rest, [...Past, Head]> : never

第二引数に配列に変換中の文字列を受け取れるようにして、変換終わるまでは再帰的に呼ぶようにした。

別解

解答見たら文字列を配列に変換する型を作成している人がいた

type StringToArray<S extends string> = S extends `${infer F}${infer R}` ? [F, ...StringToArray<R>] : [];
type LengthOfString<S extends string> = S extends `` ? 0 : StringToArray<S>[`length`];

こちらの方がやっていることが分かりやすくて良い。無駄に第二引数とかもないし。

yutake27yutake27

Flatten

配列を一次元配列に変換

解答

配列の中身を取り出して、取り出した中身が配列だったら再帰的にFlattenを呼び出し、配列でない場合は取り出した要素を返す みたいにすればいけると考えた。

type Flatten<T extends any[]> = T extends [infer Head, ...infer Rest] ? Head extends any[]? [...Flatten<Head>, ...Flatten<Rest>] : [Head, ...Flatten<Rest>] : []

その考えをそのまま実装したら通った。

yutake27yutake27

Append to object

object, key, valueを受け取ってobjectにkey, valueを追加する

解答

初め、シンプルに

type AppendToObject<T, U extends string, V> = T & {U, V}

でいけると思ったが、これだと{U, V}のUが型ではなくシンプルに値になってしまうのでUというkeyになってしまいダメだった。

代わりにRecordを使って

type AppendToObject<T, U extends string, V> = T & Record<U, V>

としたところ、Uが型として認識されたがEqualとは判定されなかった(型チェック関数的に通らなかっただけの気がする)

その後Mapped typeを試してみたらすんなり行けた

type AppendToObject<T, U extends string, V> = {
  [K in keyof T | U]: K extends keyof T ? T[K] : V
}
yutake27yutake27

Absolute

正の数を出力

解答

文字列に変換して先頭に- がついていたら外せばよさそうだった

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

そのままの感じで実装したらあっけなく通った

この辺の文字列変換のものであれば大体通るようになった

yutake27yutake27

String to Union

文字列をUnionに変換する

解答

先頭から1文字ずつ取り出して再帰的にUnionでくっつけていけばいけそうだった。

type StringToUnion<T extends string> = T extends `${infer Head}${infer Rest}` ? Head | StringToUnion<Rest> : never

上記方針でそのままあっけなく通った。

yutake27yutake27

Merge

2つのobjectをマージする。共通プロパティが存在する場合は第2引数のオブジェクトのプロパティが優先される。

解答

数日前にやったAppend to objectと同じようにMapped typeでいける考えて、そのまま実装したら通った。

type Merge<F, S> = {
  [k in keyof F | keyof S] : k extends keyof S ? S[k] : k extends keyof F ? F[k] : never
}

第2引数のオブジェクトのプロパティを優先するために先にS[k]を参照しているのがポイント

yutake27yutake27

KebabCase

キャメルケースもしくはパスカルケースの文字列を、ケバブケースに置換

解答

大文字かを判定して大文字だったらハイフン+小文字に変換すればいけそうと考えた。
ただし文字列の先頭の場合はハイフンはつけないように注意する

type KebabCase<S extends string, isHead extends boolean = true> = S extends `${infer Head}${infer Rest}`
  ? Head extends Lowercase<Head>
    ? `${Head}${KebabCase<Rest, false>}`
    :  isHead extends true
      ? `${Lowercase<Head>}${KebabCase<Rest, false>}`
      : `-${Lowercase<Head>}${KebabCase<Rest, false>}`
  : S

上記方針をもとに実装した最終的な解答こんな感じ。

初めUppercaseを使ってTUppercase<T>が一致するかどうかで大文字かどうかを判定していたが、それだと-などの記号が大文字と判定されてしまうのでLowercaseで判定するようにした。

また文字列の先頭かどうかを判定するために第二引数にisHeadという引数を作って最初だけtrueになるようにした。

条件分岐が若干煩雑だが自力解答できて嬉しみ

別解

解答見たらもっとスマートなやつがあった。

type KebabCase<S extends string> = S extends `${infer First}${infer Rest}`
? Rest extends Uncapitalize<Rest> 
  ? `${Lowercase<First>}${KebabCase<Rest>}`
  : `${Lowercase<First>}-${KebabCase<Rest>}`
: S

文字列は全て小文字にして良いのでFirstに関しては常にLowercaseで小文字に、2文字目以降で大文字が出てきた際にハイフンを前につければ良いのでRestの先頭の文字が大文字かどうかをUncapitalizeを使って判
定し、大文字の場合はハイフンをつけている。

自分の解答より条件分岐が1つ少なくてスマート。

先頭の文字を小文字化するUncapitalize覚えてなかったので覚えておく。

上級のSnakeCaseも全く同じ要領でできた

type SnakeCase<T extends string> = T extends `${infer Head}${infer Rest}`
  ? Rest extends Uncapitalize<Rest>
    ? `${Lowercase<Head>}${SnakeCase<Rest>}`
    : `${Lowercase<Head>}_${SnakeCase<Rest>}`
  : T

同じく上級のCamelCaseもほぼ同じだった

type CamelCase<S extends string, isHead extends boolean = true> = S extends `${infer Head}${infer Rest}`
  ? Rest extends `_${infer _Rest}`
    ? `${Head}${CamelCase<Capitalize<_Rest>, false>}`
    : isHead extends true
      ? `${Lowercase<Head>}${CamelCase<Uncapitalize<Rest>, false>}`
      : `${Head}${CamelCase<Uncapitalize<Rest>, false>}`
  : S
yutake27yutake27

Diff

二つのオブジェクトで非共通のプロパティを抽出

解答

前に考えたことがあったので一瞬だった

type Diff<O, O1> = Omit<O & O1, keyof O & keyof O1>

別解

先にOとO1のUnionを取る手もある

type Diff<O, O1> = Omit<O & O1, keyof O | O1>
yutake27yutake27

AnyOf

Trueとなる要素が含まれているかリスト中にどうかを判定

解答

trueとなる要素はたくさんありすぎて判定が難しいので、falseとなる要素を集めてFalseの判定を行い、falseでない=trueである要素が含まれていたらtrueを返す というような流れでいけると考えた

この方針を実装した最終的な解答が以下

type FalsiveValues = [0, '', false, [], {}, undefined, null]
type Includes<T extends readonly any[], U> = T extends [infer First, ...infer Rest]
  ? Equal<First, U> extends true
    ? true
    : Includes<Rest, U>
  : false
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
  ? Includes<FalsiveValues, First> extends true
    ? AnyOf<Rest>
    : true
  : false

falseとなる要素を集めたリストを作成してこのリストに含まれているかどうかでfalseかどうかを判定している

初め

type Falsive = 0 | '' | false | [] | {} | undefined | null
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
  ? First extends Falsive
    ? AnyOf<Rest>
    : true
  : false

としていたがFirst extends Falsiveの判定がうまくいかなかった。そのため無駄にリストにしてリストに含まれているかどうかで判定した。

別解

First extends Falsiveの方向性も間違っていなかったみたい。

空のオブジェクトの書き方がよくなかったよう。

type Falsive = 0 | '' | false | [] | Record<string, never> | undefined | null
type AnyOf<T extends readonly any[]> = T extends [infer First, ...infer Rest]
  ? First extends Falsive
    ? AnyOf<Rest>
    : true
  : false

のように{}ではなく、Record<string, never>にすると通る。

yutake27yutake27

IsNever

neverを判定

解答

Permutationで既にやった。neverをそのまま判定できないがリストにすると判定できる

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

IsUnion

Union型を判定

解答

これもPermutationの時に学んだのを覚えていた。

Union distributionが条件判定の際に発生するのでわざと発生させて、分配前後で値が異なっているかどうかを見ることでUnionかそうでないかを判定する

type IsNever<T> = [T] extends [never] ? true : false
type IsUnion<T, U = T> = IsNever<T> extends true
  ? false
  : U extends T
    ? [T] extends [U]
      ? false
      : true
    : never

neverが与えられた際にfalseを返すためにIsNeverを使用した。

yutake27yutake27

ReplaceKey

ユニオン、ユニオン型のkey、Replace後のオブジェクトを受け取ってユニオンのReplaceをするReplaceKeysを実装

type ReplacedNodes = ReplaceKeys<Nodes, 'name' | 'flag', {name: number, flag: string}> // {type: 'A', name: number, flag: string} | {type: 'B', id: number, flag: string} | {type: 'C', name: number, flag: string} // would replace name from string to number, replace flag from number to string.

type ReplacedNotExistKeys = ReplaceKeys<Nodes, 'name', {aa: number}> // {type: 'A', name: never, flag: number} | NodeB | {type: 'C', name: never, flag: number} // would replace name to never

解答

Union distributionを適切に扱えるかどうかが試されている問題

Uは最初にdistribution発生してほしいのでU extends Uでdistributionを発生させる。
Tはなるべくdistributionしてほしくない(上の方でdistributionするとオブジェクトがたくさん連なったUnionになってしまう)。条件に使うとどうしてもdistributionが発生するので下の方で条件として使用してfalseの場合はneverを返すようにする。こうするとprimitive | never => primitiveになる。

最終的な解答は以下

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

別解

解答見たらunion distributionを意識せずとも解けるようだった。

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

こんなんでいけるらしい。Uは自動的にdistributionされるっぽい。Mapped typeでもdistributionは発生するらしい。
また、Tはちゃんとneverが返るようにすれば良いみたい。

yutake27yutake27

Remove Index Signature

オブジェクトからIndex signatureを削除

解答

難しかった。全く思い浮かばず久しぶりに自力正解できなかった

こんな感じで一つ一つstring, number, symbolを判定していくと良いらしい

type RemoveIndexSignature<T> = {
  [key in keyof T as string extends key ? never : number extends key ? never : symbol extends key ? never : key]: T[key]
}
yutake27yutake27

Percentage parser

/^(\+|\-)?(\d*)?(\%)?$/の正規表現に従う文字列をparseしてリストで返す問題

解答

まず先頭の文字に来る+ | -をparseして、その後最後に来る%をparseした

type SuffixParser<A extends string> = A extends `${infer Rest}%` ? [Rest, '%'] : [A, '']
type PercentageParser<A extends string> = A extends `${infer First}${infer Rest}`
  ? First extends '+' | '-'
    ? [First, ...SuffixParser<Rest>]
    : ['', ...SuffixParser<A>]
  : ['', '', '']
yutake27yutake27

DropChar

文字列から指定された文字を除外

解答

1文字ずつ見ていって、指定された文字をextendしているか素直に判定していったら通った

type DropChar<S extends string, C extends string> = S extends `${infer First}${infer Rest}`
  ? First extends C
    ? DropChar<Rest, C>
    : `${First}${DropChar<Rest, C>}`
  : ''
yutake27yutake27

PickByType

typeを指定して、objectからtypeに合致するプロパティを取り出す

解答

Mapped typeを使ってプロパティが指定されたtypeをextendsしているかを判定すれば良い

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

as T[k]がポイント

yutake27yutake27

StartsWith

指定された文字列から始まるかどうかを判定

解答

そのまま

type StartsWith<T extends string, U extends string> = T extends `${U}${any}` ? true : false
yutake27yutake27

EndsWith

指定された文字列で終わるかどうかを判定

解答

StarsWithと同じくそのまま

type EndsWith<T extends string, U extends string> = T extends `${any}${U}` ? true : false
yutake27yutake27

PartialByKeys

PartialByKeys<T extends object, K extends keyof T>が与えられたときにプロパティKをオプショナルにする

解答

type PartialByKeys<T, K extends keyof T= keyof T> = {
  [k in keyof T as k extends K ? k : never]? : T[k]
} & {
  [k in keyof T as k extends K ? never: k] : T[k]
}

こんな感じにしたが通らずだった。
オプショナルにした結果{ k: T[k] | undefined }になってしまうのが原因かと思ったが違った。

解答見たらオブジェクトを一つにまとめるMergeが必要だった。
Mergeがあれば中身はもっと省略してよかった。

type Merge<T> = {
  [k in keyof T]: T[k]
}
type PartialByKeys<T, K extends keyof T= keyof T> = Merge<Partial<Pick<T, K>> & Omit<T, K>>
yutake27yutake27

RequiredByKeys

Keyで指定されたプロパティをRequiredにする

解答

PartialByKeysとほぼ同じ。RequiredとOptionalの違い。

type RequiredByKeys<T, K extends keyof T = keyof T> = Merge<Required<Pick<T, K>> & Omit<T, K>>
yutake27yutake27

ObjectEntries

objectを受け取り、objectの各パラメータとパラメータの型のリストのユニオンを作成(例 ['name', string] | ['age', number]

解答

objectのキーを取る方法が最初思いつかなかった。単純にtype ObjectEntries<T extends object> = k in keyof Tのようなことはできなかった。

他の問題でやったように第二引数を使ってkeyof Tを初期値に与えることでkeyが変数として扱えることに気づいた。

これを踏まえて最初以下のような解答にした。
各keyごとにユニオンを取るためにunion distributionを発生させている。

type ObjectEntries<T extends object, U extends keyof T = keyof T> = U extends U? [U, T[U]] : never

これだとオプショナル引数のプロパティにundefinedが含まれてしまいダメだった。

undefinedを打ち消すNonUndefinedを導入してこれに対処した。

最終的な解答は以下。

type NonUndefined<T> = Equal<T, undefined> extends true ? undefined : Exclude<T, undefined>
type ObjectEntries<T extends object, U extends keyof T = keyof T> = U extends U? [U, NonUndefined<T[U]>] : never
yutake27yutake27

Shift

配列から先頭の要素を抜いて返す

解答

特筆すべき点なし

type Shift<T extends any[]> = T extends [any, ...infer U] ? [...U] : []
yutake27yutake27

Tuple to Nested Object

type a = TupleToNestedObject<['a'], string> // {a: string}
type b = TupleToNestedObject<['a', 'b'], number> // {a: {b: number}}
type c = TupleToNestedObject<[], boolean> // boolean. if the tuple is empty, just return the U type

こんな感じの型を作る

解答

先頭の要素を取り出して、先頭の要素と残りの要素を再帰的にTupleToNestedObjectしたもののタプルを作ればいけた。

type TupleToNestedObject<T extends unknown[], U> = T extends [infer V, ...infer W]
  ? V extends string
    ? Record<V, TupleToNestedObject<W, U>>
    : never
  : U
yutake27yutake27

Reverse

配列の順序を逆順にする

解答

先頭から取り出して最後に入れるという操作を再起的に行えば良い

type Reverse<T extends unknown[]> = T extends [infer U, ...infer V] ? [...Reverse<V>, U] : []
yutake27yutake27

Flip Arguments

関数の引数の型の順序を逆順にする。このとき引数の順序自体は逆順にしない

解答

関数の型をgenericsで表現するやり方を若干忘れていた。(..args: any[]) => anyでいけることを思い出した。

関数の引数が取れたら、後は引数の型を1つ前のReverseで逆順にしてやるだけでいけた。

type Reverse<T extends unknown[]> = T extends [infer U, ...infer V] ? [...Reverse<V>, U] : []
type FlipArguments<T extends (...args: any[]) => any> = T extends (...args: infer U) => infer V
  ?  (...args: Reverse<U>) => V
  : never
yutake27yutake27

MinusOne

1引いた数を返す

自分の解答

配列の長さを使って良い感じにできないかと思い↓のような解答を作成した。

type MinusOne<T extends number, A extends any[] = []> = [...A, any] extends { length: T} ? A['length'] : MinusOne<T, [...A, any]>

これだとある程度大きい数字になると再帰が深すぎてダメ&マイナスになる場合にダメだった。

他の人の解答

文字列として扱って頑張って9だったら8に, 8だったら7にとかを愚直にやっていた。
実装は省略。

yutake27yutake27

FlattenDepth

指定された数だけFlattenする

自分の解答

数をどんどん引いていって、指定された数になったら再帰を止めればいけそうだと思った
数を引くために前にやったMinusOneの実装を持ってきた。

type MinusOne<T extends number, A extends any[] = []> = [...A, any] extends { length: T } ? A['length'] : MinusOne<T, [...A, any]>
type FlattenDepth<T, U extends number = 1> = U extends 0
  ? T 
  : T extends [infer Head, ...infer Rest]
    ? Head extends unknown[]
      ? [...FlattenDepth<Head, MinusOne<U>>, ...FlattenDepth<Rest, U>]
      : [Head, ...FlattenDepth<Rest, U>]
    : T

解説

ほとんど方針は同じだがMinusOneを使わずにCountしていくやり方で正解している人が多かった。MinusOneだと再帰の数で怒られたが、これだとFlattenし終わったら途中で再帰が終了する(指定された数だけ再帰を繰り返すわけではない)ので問題ない。

type FlattenDepth<T, U extends number = 1, Count extends unknown[] = []> = Count['length'] extends U
  ? T 
  : T extends [infer Head, ...infer Rest]
    ? Head extends unknown[]
      ? [...FlattenDepth<Head, U, [...Count, any]>, ...FlattenDepth<Rest, U, Count>]
      : [Head, ...FlattenDepth<Rest, U, Count>]
    : T
yutake27yutake27

BEM style string

CSSの命名規則の一つであるBEMを実装してみようというやつ。
Block, Element, Modifierが与えられたときに
Block__Element--Modifierという形式にする。Blockは文字列でElementとModifierは文字列の配列。

例えば以下のような感じ

type testCase1 = Equal<BEM<'btn', ['price'], ['warning', 'success']>, 'btn__price--warning' | 'btn__price--success' >

解答

配列からユニオンを作成するのはT[number]でいけることを覚えていたのでそれを使う方針にした。

あとはElementとModifierが空の配列の場合があるのでその例外処理だけ気をつけて実装した。

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

InorderTraversal

2分木をinorder順(最も左の子が最優先でleft → parent → rightの順で見ていくやつ)で見ていってlistを作成

解答

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

こんなシンプルなのを作ったが再帰が深すぎる&Unionが複雑すぎると怒られた。(ただしテストケースは通っている)

理由がよく分からず解説見たらUnion distributionが発生してしまっているからっぽい。
T extends TreeNode | nullなので最初の条件判定でdistributionが発生してしまっている。

Union distributionを発生させないか、null checkをすることで解決できる。

union distributionを発生させない

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

null check案

type InorderTraversal<T extends TreeNode | null> =
  T extends TreeNode
  ? [
      ...(
        T['left'] extends TreeNode
        ? InorderTraversal<T['left']>
        : []
      ),
      T['val'],
      ...(
        T['right'] extends TreeNode
        ? InorderTraversal<T['right']>
        : []
      ),
    ]
  : []
yutake27yutake27

Flip

objectのkeyとpropertyを入れ替える

解答

最初シンプルに入れ替えれば良いだけだと思った

type Flip<T extends object> = {
  [k in keyof T as T[k] extends PropertyKey ? T[k] : never]: k
}

上記コードを書いたところ↓のテストケースで通らなかった。

Expect<Equal<{ 3.14: 'pi'; true: 'bool' }, Flip<{ pi: 3.14; bool: true }>>>

trueがPropertyKey(string | number | symbol)ではないのでうまくいっていなかった。

ダメもとでtrueを文字列で"true"として返したらうまくいった。

最終的な解答は↓

type Flip<T extends object> = {
  [k in keyof T as T[k] extends PropertyKey
    ? T[k]
    : T[k] extends true
      ? "true"
      : T[k] extends false
        ? "false"
        : never
  ]: k
}

他の人の解答

もっと簡単にできたみたい。

type Flip<T extends Record<string, any>> = {
  [Key in keyof T as `${T[Key]}`]: Key
}

こんな感じ

yutake27yutake27

Fibonacci Sequence

フィボナッチ数列を作成

解答

非常に面倒だが、足し算とマイナス1を計算する型を作成してゴリ押ししたらいけた。

type NumberToArray<T extends number, Arr extends any[] = []> = Arr['length'] extends T ? Arr : NumberToArray<T, [...Arr, any]>
type Plus<T extends number, U extends number> = [...NumberToArray<T>, ...NumberToArray<U>]['length']
type MinusOne<T extends number, A extends any[] = []> = [...A, any] extends { length: T} ? A['length'] : MinusOne<T, [...A, any]>
type Fibonacci<T extends number, Current extends number = 1, Before extends number = 0> = T extends 1
  ? Current
  : Plus<Current, Before> extends number
    ? Fibonacci<MinusOne<T>, Plus<Current, Before>, Current>
    : never

他の人の解答

わざわざ定義しなくても現在の再帰の数、現在の数、一つ前の数の3つを引数にして再帰すればいける

こんな感じ

type Fibonacci<
  T extends number,
  Rec extends any[] = [any],
  Current extends any[] = [any],
  Prev extends any[] = []
> = Rec['length'] extends T
  ? Current['length']
  : Fibonacci<T, [...Rec, any], [...Current, ...Prev], Current>
yutake27yutake27

AllCombinations

文字列を受け取って、その文字列を構成する文字の組み合わせを全て返すというやつ

解答

難しかった。Union distributionをうまく使うんだろうなと思い、色々触ってみていたがパッとうまい方法が見つからなかった。

良い方針が見つけられず、自力正解できなかった。

方針

ヒントをもらい次の方針でいけばできそうだった。

  1. 文字列から各文字のUnion型に変換
  2. ユニオンから1文字とって、その文字自身を返す場合、その文字を使用してPermutateする場合、その文字を使用しないでAllConbinationsを再帰的に実行する場合に分ける

これを実装してみたのが以下

type StringToUnion<S extends string> = S extends `${infer Head}${infer Rest}`
  ? Head | StringToUnion<Rest>
  : never

type Permutation<T extends string, U extends string = T> = T extends T
  ? [U] extends [T]
    ? `${T}`
    : `${T}${Permutation<Exclude<U, T>>}`
  : never
type IsNever<T> = [T] extends [never] ? true : false

type AllCombinations<S extends string, T extends string = StringToUnion<S>, U extends string = T> = IsNever<T> extends true
  ? ""
  : T extends T
    ? [U] extends [T]
      ? S | ""
      : S | Permutation<U> | AllCombinations<Exclude<U, T>>
    : never

コードがだいぶ長くなってしまったが、これで通った。
""がユニオンに含まれないようにするのがポイント。

他の人の解答を見たらもっと簡潔なものもあった。

yutake27yutake27

Greater Than

2つのNumberを受け取って、第一引数が第二引数よりも大きいかったらtrue, それ以外はfalseを返す

解答

数字を配列に変換して、配列から要素を1つずつ取り出していき、先に第2引数の数字の配列が空になったらtrueを返すようにした。

type NumberToArray<T extends number, Arr extends any[] = []> = Arr['length'] extends T ? Arr : NumberToArray<T, [...Arr, any]>
type GreaterThan<T extends number, U extends number, TA extends any[] = NumberToArray<T>, UA extends any[] = NumberToArray<U>> = TA extends []
  ? false
  : UA extends []
    ? true
    : TA extends [infer TAHead, ...infer TARest]
      ? UA extends [infer UAHead, ...infer UARest]
        ? GreaterThan<T, U, TARest, UARest>
        : never
      : never

ただ、これだと数値が大きい時に再起が深すぎてエラーになってしまった。
他の人は数字を文字列として扱うことで再帰が深くならないようにしていた。ただ条件分けが煩雑で面倒なのでこの問題は諦めた。

追記

そこまで複雑ではない方針でいけそうだった

  1. 桁数を確認して多かったらgreater
  2. 桁数同じだったら先頭の数字を比較して大きかったらgreater
  3. 数字が同じだったら次の桁で2を繰り返す
yutake27yutake27

Zip

pythonのzip関数と同じやつ

type exp = Zip<[1, 2], [true, false]> // expected to be [[1, true], [2, false]]

解答

両方の配列から先頭の要素を取り出して行って、取り出せなくなったらループ終了

type Zip<T extends any[], U extends any[]> = T extends [infer THead, ...infer TRest]
  ? U extends [infer UHead, ...infer URest]
    ? [[THead, UHead], ...Zip<TRest, URest>]
    : []
  : []

シンプルな方針でいけた。

yutake27yutake27

IsTuple

Tupleを判定

解答

Tupleの定義がよく分からなかったので、とりあえずテストケースが通るような条件分岐を書いた

type IsNever<T> = [T] extends [never] ? true : false
type IsTuple<T> = IsNever<T> extends true
  ? false
  : T extends [] | [any] | readonly [any]
    ? true
    : false

これで通った。がこれで良いのか??(ダメそう)

他の人の解答

色々あったが↓が多そうだった。

type IsTuple<T> = [T] extends [never] 
? false 
: T extends readonly unknown[]   
    ? number extends T['length']
      ? false
      : true
    : false

これは↓の性質を活かしたものらしい。

type Length<T> = T extends readonly unknown[] ? T['length'] : never
type hoge = Length<number[]> // number
yutake27yutake27

Chunk

↓を満たす関数

type exp1 = Chunk<[1, 2, 3], 2> // expected to be [[1, 2], [3]]
type exp2 = Chunk<[1, 2, 3], 4> // expected to be [[1, 2, 3]]
type exp3 = Chunk<[1, 2, 3], 1> // expected to be [[1], [2], [3]]

解答

再帰の回数を3つ目の引数で計測して、その回数になったら次の再帰に移るという方法でいけた。
再帰の数よりも配列の数の方が短い場合に、途中までの配列を返さなければいけないことに注意。

type Chunk<T extends any[], U extends number, V extends any[] = []> = V['length'] extends U
  ? [V, ...Chunk<T, U, []>]
  : T extends [infer Head, ...infer Rest]
    ? Chunk<Rest, U, [...V, Head]>
    : V extends []
      ? []
      : [V]
yutake27yutake27

Fill

  Expect<Equal<Fill<[1, 2, 3], true>, [true, true, true]>>,
  Expect<Equal<Fill<[1, 2, 3], true, 0, 1>, [true, 2, 3]>>,
  Expect<Equal<Fill<[1, 2, 3], true, 1, 3>, [1, true, true]>>,
  Expect<Equal<Fill<[1, 2, 3], true, 10, 0>, [1, 2, 3]>>,
  Expect<Equal<Fill<[1, 2, 3], true, 10, 20>, [1, 2, 3]>>,
  Expect<Equal<Fill<[1, 2, 3], true, 0, 10>, [true, true, true]>>,

こんな感じでArrray, value, start, endを受け取ってstart <= index < endまでのindexの要素をvalueに置き換える。

解答

start <= index < endをうまく判定してやれば良い。
ちょっと前にGreater thanをやったのでそれが使えそうだと思った。

第5引数に変換後の値を詰めるarrayを作成して、そのarrayの長さで今のindexを判定することにした。

ちょっと長くなってしまったが、↓の感じで行けた。

type GreatherThan = // GreatherThan参照

type GreaterEqual<T extends number, U extends number> = T extends U
  ? true
  : GreaterThan<T, U> extends true
  ? true
  : false

type Fill<
  T extends unknown[],
  N,
  Start extends number = 0,
  End extends number = T['length'],
  A extends unknown[] = [],
> = T extends [infer Head, ...infer Rest]
  ? Fill<Rest, N, Start, End, [...A, GreaterEqual<A['length'], Start> extends true
    ? GreaterThan<End, A['length']> extends true
      ? N
      : Head
    : Head
  ]>
  : A
yutake27yutake27

Without

第一引数の配列から第二引数で指定された配列または値を除外する

解答

配列の中に値が含まれているかどうかを判定するIncludeを定義し、それを利用し配列の要素を一つずつ見ていくことで除外していった。

type Include<Target extends any[], Value extends any> = Target extends [infer Head, ...infer Rest]
  ? Equal<Head, Value> extends true
    ? true
    : Include<Rest, Value>
  : false

type Without<T extends any[], U extends any[] | any, Result extends any[] = []> = T extends [infer Head, ...infer Rest]
  ? U extends any[]
    ? Include<U, Head> extends true
      ? Without<Rest, U, Result>
      : Without<Rest, U, [...Result, Head]>
    : Equal<Head, U> extends true
      ? Without<Rest, U, Result>
      : Without<Rest, U, [...Result, Head]>
  : Result
yutake27yutake27

Trunc

数字または文字から整数部分のみを取り出す

解答

文字列として扱えば良い

type Trunc<T extends number | string> = `${T}` extends `${infer Int}.${any}`
  ? Int extends ''
    ? '0'
    : Int
  : `${T}`
yutake27yutake27

IndexOf

ArrayとValueが与えられらときに、Valueと一致するIndexを取得(一致する先頭のIndex)

解答

現在のIndexを保持する配列を第3引数に持っておく。
先頭から要素を見ていって一致したらそのIndexを返す

type IndexOf<T extends any[], U extends unknown, Checked extends any[] = []> = T extends [infer Head, ...infer Rest]
  ? Equal<Head, U> extends true
    ? Checked['length']
    : IndexOf<Rest, U, [...Checked, any]>
  : -1
yutake27yutake27

Join

配列T、文字列または数値Uを受け取り、Tの各要素の間にUを入れる

解答

配列の要素を一つずつ見てその間に入れていく。配列の各要素の間にだけ入れることに注意。

type Join<T extends unknown[], U extends string | number, Ret extends string = ''> = T extends [infer Head, ...infer Rest]
  ? Head extends string
    ? Join<Rest, U, Ret extends '' ? Head : `${Ret}${U}${Head}`>
    : never
  : Ret
yutake27yutake27

LastIndexOf

IndexOfの先頭のIndexではなく、最後のIndexを返す版

解答

先頭から見ていって一致したらIndexを更新していく

type LastIndexOf<T extends any[], U extends unknown, Checked extends any[] = [], Index extends number = -1> = T extends [infer Head, ...infer Rest]
  ? Equal<Head, U> extends true
    ? LastIndexOf<Rest, U, [...Checked, any], Checked['length']>
    : LastIndexOf<Rest, U, [...Checked, any], Index>
  : Index

先頭から見ないでTailから見る方法もある? Indexの取得がtailからだと厳しそう

yutake27yutake27

Unique

配列からユニークな要素を抜き出す

解答

配列の中に要素が含まれているかを判定するIncludeを定義して、含まれていたらスルー、含まれていなかったら追加を繰り返す。

type Include<T extends any[], Value extends any> = T extends [infer Head, ...infer Rest]
  ? Equal<Head, Value> extends true
    ? true
    : Include<Rest, Value>
  : false

type Unique<T extends any[], U extends any[] = []> = T extends [infer Head, ...infer Rest]
  ? Unique<Rest, Include<U, Head> extends true ? U : [...U, Head]>
  : U
yutake27yutake27

MapType

type StringToNumber = { mapFrom: string; mapTo: number;}
MapTypes<{iWillBeANumberOneDay: string}, StringToNumber> // gives { iWillBeANumberOneDay: number; }

こんな感じでMapを受け取って、そのMapに従って型を変える

type StringToNumber = { mapFrom: string; mapTo: number;}
type StringToDate = { mapFrom: string; mapTo: Date;}
MapTypes<{iWillBeNumberOrDate: string}, StringToDate | StringToNumber> // gives { iWillBeNumberOrDate: number | Date; }

Mapがユニオンである場合もあるので注意

解答

ユニオンがない場合はMappedTypeで超単純に実装すれば良いだけなので簡単。

だが、ユニオンの場合が難しかった。ユニオンが与えられた際にはdistributionを発生させなければいけない。MappedTypeでも自動的にdistributionが発生する場合がある([k in keyof T]のTがユニオンの場合)が今回はそれに該当しない。

R extends Rでdistributionを発生することができるが、Rがユニオンだったかどうかで分岐を発生させなければいけない。ユニオンを判定するためにRを代入したSを第3引数に置いた。

distributionを発生させた後にSがRをextendしていれば(S extends R)ユニオンではないと判断できると考えた。
しかしながらS extends Rではユニオンが判断できずテストケースが通らなかった。

試行錯誤し、S[mapFrom'] extends R['mapFrom']とすることでユニオンと判断できることがわかった。(なぜ要素までアクセスすると良いのかはよく分かっていない)

type Map = {
  mapFrom: any;
  mapTo: any;
}

type MapTypes<T extends object, R extends Map, S extends Map = R> = {
  [k in keyof T]: R extends R
    ? Equal<T[k], R['mapFrom']> extends true
      ? R['mapTo']
      : S['mapFrom'] extends R['mapFrom']
        ? T[k]
        : never
    : never
}
yutake27yutake27

ConstructTuple

numberを受け取って、そのnumberの長さのunknownの配列を作成

解答

特にコメントすることなし

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

Number Range

type result = NumberRange<2 , 9> //  | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 

↑のようなRangeを生成

解答

type NumberRange<L, H>のとき、[L, L+1, ..., H -1, H] の配列Aを作成して、A[number]でいけそう

type PlusOne<T extends number, A extends any[] = []> = A['length'] extends T ? [...A, any]['length'] : PlusOne<T, [...A, any]>
type NumberRange<L extends number, H extends number, Current extends number = L, Arr extends number[] = []> = Current extends H
  ? [...Arr, Current][number]
  : NumberRange<L, H, PlusOne<Current>, [...Arr, Current]>
yutake27yutake27

Combination

文字列の配列を受け取って、それらの全ての組み合わせを得る

解答

文字列の配列Tを受け取り、T[number]でユニオンに変換した後、Union distributionを使えば良い。

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

Subsequence

配列の部分配列を取得

// 例
Equal<Subsequence<[1, 2, 3]>, [] | [1] | [2] | [1, 2] | [3] | [1, 3] | [2, 3] | [1, 2, 3] >

解答

先頭から要素を一つずつ見ていって、その要素を使うか使わないかを判断していけば良い

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

CheckRepeatedChars

文字列中に同じ文字が2回以上含まれているかを判定

解答

配列に文字を入れていって、その配列中に既に文字が含まれていたらtrueを返す

type CheckRepeatedChars<T extends string, U extends string[] = []> = T extends `${infer Head}${infer Rest}`
  ? Includes<U, Head> extends true
    ? true
    : CheckRepeatedChars<Rest, [...U, Head]>
  : false
yutake27yutake27

FirstUniqueCharIndex

文字列を先頭から見ていって最初のUniqueな文字のIndexを返す

解答

文字列の中で、ある文字がUniqueかを返すCheckUniqueを定義して、文字列を最初から見ていった時にCheckUniqueがtrueの文字のIndexを返すという方針でできた

type CheckUnique<Str extends string, Char extends string, Appeared extends boolean = false> = Str extends `${infer Head}${infer Rest}`
  ? Head extends Char
    ? Appeared extends true
      ? false
      : CheckUnique<Rest, Char, true>
    : CheckUnique<Rest, Char, Appeared>
  : true

type FirstUniqueCharIndex<T extends string, U extends string = T, IndexList extends string[] = []> = T extends `${infer Head}${infer Rest}`
  ? CheckUnique<U, Head> extends true
    ? IndexList['length']
    : FirstUniqueCharIndex<Rest, U, [...IndexList, Head]>
  : -1
yutake27yutake27

GetMiddleElement

配列を受け取って真ん中の要素を返す(配列長が偶数の場合は2つ、奇数の場合は1つを返す)

解答

配列を先頭、末尾、それ以外に分解して分解できなくなったら配列を返す

type GetMiddleElement<T extends any[]> = T extends [infer Head, ...infer Rest, infer Tail]
  ? Rest extends []
    ? T
    : GetMiddleElement<Rest>
  : T

あまり見なかった面白い問題

yutake27yutake27

Appear only once

配列の中からUniqueな要素のみを抜き出す

解答

配列の中でUniqueかを判定するCheckUniqueを定義し、配列の要素を先頭から見ていった時にUniqueだったらその要素を配列に加え、Uniqueでなかったら配列に加えずそのままという操作を繰り返す

type CheckUnique<T extends any[], U extends unknown, Appeared extends boolean = false> = T extends [infer Head, ...infer Rest]
  ? Equal<Head, U> extends true
    ? Appeared extends true
      ? false
      : CheckUnique<Rest, U, true>
    : CheckUnique<Rest, U, Appeared>
  : true

type FindEles<T extends any[], U extends any[] = T, Ret extends any[] = []> = U extends [infer Head, ...infer Rest]
  ? CheckUnique<T, Head> extends true
    ? FindEles<T, Rest, [...Ret, Head]>
    : FindEles<T, Rest, Ret>
  : Ret
yutake27yutake27

Integer

整数かどうかを判定する

解答

整数部と少数部に分解して、少数部が0のみだったら整数、0以外が含まれていたら少数と判定した。
また、number型を除外するためにEqual<T, number> extends trueを用いた。

type ZeroOnly<T extends string> = T extends `${infer Head}${infer Rest}`
  ? Head extends '0'
    ? ZeroOnly<Rest>
    : false
  : true

type Integer<T extends number> = Equal<T, number> extends true
  ? never
  : `${T}` extends `${any}.${infer Decimal}`
    ? ZeroOnly<Decimal> extends true
      ? T
      : never
    : T
yutake27yutake27

Primitive

objectのpropertyを全てprimitiveな型に変換

解答

愚直に各primitiveかどうかを判定してそれを返すようにした。

listの場合は再帰処理が必要なのでprimitive化する型を作成した。
また、objectの場合も再帰処理が必要。

関数がobjectとして判定されてしまうので、関数かどうかはobjectかどうかを判定する前にも必要。

type PrimitiviseList<T extends any[]> = T extends [infer Head, ...infer Rest]
  ? [Primitivise<Head>, ...PrimitiviseList<Rest>]
  : []

type Primitivise<T> = T extends any[]
  ? PrimitiviseList<T>
  : T extends string
    ? string
    : T extends number
      ? number
      : T extends boolean
        ? boolean
        : T extends (...args: any) => void
          ? Function
          : never

type ToPrimitive<T extends object> = {
  [k in keyof T]: T[k] extends (...args: any) => void
    ? Function
    : T[k] extends object
      ? ToPrimitive<T[k]>
      : Primitivise<T[k]>
}

無駄に長くなってしまっている。

他の人の解答

他の人の解答見たらもっとスマートなものがあった。

type ToPrimitive<T> = T extends Function
    ? Function
    : T extends object
    ? {
          [K in keyof T]: ToPrimitive<T[K]>
      }
    : T extends boolean
    ? boolean
    : T extends string
    ? string
    : T extends number
    ? number
    : never;

一番外側でmapped typesを使わずに、objectだった場合にmapped typesを使うようにしている。
こうすることでわざわざ他の型を定義せずに済む。(リストがどうやって処理されているかはいまいちわかっていない)

yutake27yutake27

DeepMutable

全てmutableに変換

解答

- readonlyでreadonlyを外せるのでそれを使う。

type DeepMutable<T extends object> = {
  - readonly [k in keyof T]: T[k] extends Function
    ? T[k]
    : T[k] extends object
      ? DeepMutable<T[k]>
      : T[k]
}

Functionがobjectとして判定されるのに気をつける

yutake27yutake27

All

第一引数で与えられる配列の全ての要素が第二引数と等しいかを判定

解答

先頭から全ての要素が等しいかどうかを判定していく

type All<T extends any[], U> = T extends [infer Head, ...infer Rest]
  ? Equal<Head, U> extends true
    ? All<Rest, U>
    : false
  : true
yutake27yutake27

Filter

第一引数で与えられる配列の中から、第二引数で与えられるプリミティブな値を継承しているもののみの配列をフィルタリングして返す

解答

シンプルに先頭から要素を見ていって継承しているかを判定する

type Filter<T extends any[], P, Ret extends any[]= []> = T extends [infer Head, ...infer Rest]
  ? Head extends P
    ? Filter<Rest, P, [...Ret, Head]>
    : Filter<Rest, P, Ret>
  : Ret
yutake27yutake27

Combination key type

配列の中からペアの組み合わせを求める
ペアを作る際、配列の順序が早い方が必ず先に来るようにする

解答

配列を先頭から取り出していき、先頭を使う場合使わない場合に分け、使う場合は保存しておく。
既に保存されたものがある場合はそれを使用して文字列を作る。
これを繰り返すことでできた。

type ModifierKeys = ['cmd', 'ctrl', 'opt', 'fn']

type _Combs<T extends unknown[], First extends string = ''> = T extends [infer Head, ...infer Rest]
  ? First extends ''
    ? Head extends string
      ? _Combs<Rest, `${Head}`> | _Combs<Rest, ''>
      : never
    : Head extends string
      ? `${First} ${Head}` | _Combs<Rest, First>
      : never
  : never

type Combs = _Combs<ModifierKeys>

他の人の解答

もっとスマートなやり方があった。

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

type Combs = Combination<ModifierKeys>

こんな感じで残りの要素をRest[number]でdistributionしながら指定している。すごい。

また、配列から要素を取り出す際にも[infer Head, ...infer Rest]だと型がunknownになってしまうが
[infer Head extends string, ...infer Rest extends string[]]とすることでstring型になるようにしている。このやり方初めて知った!!

勉強になった。

yutake27yutake27

ReplaceFirst

タプルTに最初に現れるSをRに置き換えるReplaceFirst<T, S, R>型を実装

解答

1回変換したらS, Rにnullを入れる

type ReplaceFirst<T extends readonly unknown[], S, R, Ret extends unknown[] = []> = T extends [infer Head, ...infer Rest]
  ? Head extends S
    ? ReplaceFirst<Rest, null, null, [...Ret, R]>
    : ReplaceFirst<Rest, S, R, [...Ret, Head]>
  : Ret
yutake27yutake27

Transpose

行列の転置をする

解答 WIP

↓のようにとりあえず配列を分解する型を作成した。
配列を分解した後、うまく結合すれば転置できそうだが、結合のやり方が思いついていない。
一旦置いておく。

type Split<T extends number[], Ret extends number[][] = []> = T extends [infer Head extends number, ...infer Rest extends number[]]
  ? Split<Rest, [...Ret, [Head]]>
  : Ret