Closed16

type-challengesに挑戦【初級編】

さとうさとう

Hello World

HelloWorld型を変えて、型エラーを直す

type HelloWorld = any
type test = Expect<Equal<HelloWorld, string>> // エラー

回答

type HelloWorld = string

解説

なし

メモ

エラーチェックで使用している型について

Expect型

https://github.com/type-challenges/type-challenges/blob/e267da6e5eed15286bee4184ad1acb5df6eb177a/utils/index.d.ts#L1
Tに渡される型がtrueであることをextends trueで制限している
つまりTtrue以外だとエラー

Equal型

https://github.com/type-challenges/type-challenges/blob/e267da6e5eed15286bee4184ad1acb5df6eb177a/utils/index.d.ts#L7-L9
Conditional typesを使いT extends XT extends Yの結果を比較している
つまりXYが同じ型でなければfalseになる

さとうさとう

Pick

組み込みの型ユーティリティPick<T, K>を使用せず、TからKのプロパティを抽出する型を実装する

interface Todo {
  title: string
  description: string
  completed: boolean
}

type TodoPreview = MyPick<Todo, 'title' | 'completed'>

const todo: TodoPreview = {
    title: 'Clean room',
    completed: false,
}

Todo型から'title' | 'completed'のプロパティだけを抜き出し、TodoPreview型として使うということ
Pickを使用すると以下

type TodoPreview = Pick<Todo, 'title' | 'completed'>

回答

 type MyPick<T, K extends keyof T> = { [key in K]: T[K] }

解説

  • K extends keyof Tで、KTのキー(プロパティ名)であることを制限
    Todoのキーは'title' | 'description' | 'completed'
  • { [key in K]: T[key] }は、Mapped Typeを使いKの各キーを順に取り出して、対応するTのプロパティ型を新しいオブジェクト型のプロパティ型として設定

メモ

keyof

keyofはオブジェクトの型からプロパティ名を型として返す型演算子です。

https://typescriptbook.jp/reference/type-reuse/keyof-type-operator

Mapped Types

Mapped Typesは主にユニオン型と組み合わせて使います。
インデックス型と同じようにキーの制約として使用することができます。

https://typescriptbook.jp/reference/type-reuse/mapped-types

さとうさとう

Readonly

組み込みの型ユーティリティReadonly<T>を使用せず、Tのすべてのプロパティを読み取り専用にする型を実装する

interface Todo {
  title: string
  description: string
}

const todo: MyReadonly<Todo> = {
  title: "Hey",
  description: "foobar"
}

todo.title = "Hello" // Error: cannot reassign a readonly property
todo.description = "barFoo" // Error: cannot reassign a readonly property

回答

 type MyReadonly<T> = { readonly [K in keyof T]: T[K] }

解説

  • keyof Tは、Tのプロパティ名すべてのユニオン型("title" | "description")を表す
  • [K in keyof T]Mapped Typeで、Tのすべてのプロパティ名Kを順番に取り出す
  • ⭐️readonlyを付けて各プロパティを読み取り専用に
  • T[K]はプロパティKの型を意味し、元の型のまま値の型を維持

メモ

readonly

https://typescript-jp.gitbook.io/deep-dive/type-system/readonly

さとうさとう

Tuple to Object

タプルを受け取り、その各値のkey/valueを持つオブジェクトの型に変換する型を実装する

const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const

type result = TupleToObject<typeof tuple> // expected { 'tesla': 'tesla', 'model 3': 'model 3', 'model X': 'model X', 'model Y': 'model Y'}

回答

type TupleToObject<T extends readonly (string | number | symbol)[]> = {
  [P in T[number]]: P
}

解説

  • ⭐️T[number]で配列やタプルの全要素の型をユニオン型として取り出す
    → 展開すると'tesla', 'model 3', 'model X', 'model Y'
  • [P in T[number]]Mapped Typeで、ユニオン型の各要素Pを順にキーとして取り出す
  • { [P in T[number]]: P }でキーと同じ値を持つオブジェクト型を作成

メモ

タプルとは?

  • 要素の数と型が決まっている配列
  • 例えば[string, number]は「最初が文字列、次が数値」の2つの要素を持つ
  • 配列との違いは、配列は同じ型の要素が何個でも並ぶのに対し、タプルは型も個数も固定されている点
    https://typescriptbook.jp/reference/values-types-variables/tuple
さとうさとう

First of Array

配列Tを受け取り、その最初のプロパティの型を返すFirst<T>を実装する

type arr1 = ['a', 'b', 'c']
type arr2 = [3, 2, 1]

type head1 = First<arr1> // expected to be 'a'
type head2 = First<arr2> // expected to be 3

回答

type First<T extends any[]> = T extends [] ? never : T[0]

解説

  • ⭐️T extends [] ?で配列Tが空かどうかを判定し、空ならtrueとしてneverを返す
  • T[0]は配列の先頭の要素の型を指すので、空でない場合T[0]がそのまま返る
  • 結果として「配列が空ならnever、そうでなければ最初の要素の型」を返す型になる

これだけでもいいのでは?と思ったが、空配列の場合undefinedになるのでneverを返した方が安全

type First<T extends any[]> = T[0];

メモ

never型

never型は「値を持たない」を意味するTypeScriptの特別な型です。

https://typescriptbook.jp/reference/statements/never

さとうさとう

Length of Tuple

タプルTを受け取り、そのタプルの長さを返す型Length<T>を実装する

type tesla = ['tesla', 'model 3', 'model X', 'model Y']
type spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT']

type teslaLength = Length<tesla>  // expected 4
type spaceXLength = Length<spaceX> // expected 5

回答

type Length<T extends readonly any[]> = T['length']

解説

  • T extends readonly any[]で、Tはタプルまたは読み取り専用の配列であることを制限
    readonlyがないとas constで定義されたリテラルタプルが代入できない
  • T['length']で、配列やタプルのlengthプロパティを使って要素数を取得

メモ

as const

変数宣言のときに、末尾にas constをつけるとその値をreadonlyにした上で、リテラル型にしてくれます。

→ 型はreadonly ['a', 'b']になり、各要素の型も'a' | 'b'ではなく"a" や "b"などのリテラル型になる
https://typescriptbook.jp/reference/values-types-variables/const-assertion

さとうさとう

Exclude

組み込みの型ユーティリティExclude <T, U>を使用せず、Uに割り当て可能な型をTから除外する型を実装する

type Result = MyExclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

Excludeを使用すると以下

type Result = Exclude<'a' | 'b' | 'c', 'a'>  // 'b' | 'c'

回答

type MyExclude<T, U> = T extends U ? never : T

解説

  • T extends U ? never : TはConditional Typeを使用して、TUに割り当て可能(T extends U)かどうかをチェック
    → 割り当て可能ならnever、不可能ならTを残す = TからUを取り除く
さとうさとう

Awaited

Promise<ExampleType>という型がある場合、どのようにしてExampleTypeを取得すればよいか

type ExampleType = Promise<string>

type Result = MyAwaited<ExampleType> // string

Awaitedを使うと以下

type Result = Awaited<ExampleType> // string

回答

type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U>
  ? U extends PromiseLike<any>
    ? MyAwaited<U>
    : U
  : never;

解説

  • ⭐️T extends PromiseLike<any>で、TPromiseまたはthen()を持つことを制限
    Promise<T>Promise<Promise<T>>に対応可能に
  • ⭐️T extends PromiseLike<infer U>で、PromiseLikeの中身の型をUとして取り出す
    (例:Promise<string>U = string
  • U extends PromiseLike<any>で、UがまたPromiseであれば再びMyAwaited<U>を実行(再帰処理)し、そうでない場合Uを返す
  • TPromiseLikeでない場合はneverを返す

🗣️ むずー

メモ

PromiseLike

Promiseのcatchメソッドがない版
https://github.com/microsoft/TypeScript/blob/c6a1812e0cc5e0c878d222fa80bc3225f72c136d/src/lib/es5.d.ts#L1519-L1527

infer

型の中から特定の型を“推論”(infer)して取り出すことができる
使える場所はconditional typesのextendsの条件部分に限定される
https://typescriptbook.jp/reference/type-reuse/conditional-types/infer

さとうさとう

If

Ctruthyである場合の戻り値の型TCfalsyである場合の戻り値の型Fを受け取るIfを実装する

type A = If<true, 'a', 'b'>; // expected to be 'a'
type B = If<false, 'a', 'b'>; // expected to be 'b'

回答

type If<C extends boolean, T, F> = C extends true ? T : F

解説

  • C extends booleanCbooleanに制限
  • C extends true ? T : FでConditional typesを使用しCtrueなら型Tを返し、そうでなければ型Fを返す
さとうさとう

Concat

JavaScriptのArray.concat関数を型システムに実装する

type Result = Concat<[1], [2]>; // expected to be [1, 2]

実際に配列を結合するのではなく、「結合後の配列の形」を型として表現するということ

回答

type Tuple = readonly unknown[];
type Concat<T extends Tuple, U extends Tuple> = [...T, ...U];

解説

  • Length of Tupleでやったように、readonlyを付けることでas constな配列にも対応可能に
  • T extends Tuple, U extends Tupleで、TUは両方とも配列(またはタプル)型に制限
  • ⭐️[...T, ...U]で、スプレッド構文を使いTUの要素を結合した新しいタプル型を作成
さとうさとう

Includes

JavaScriptのArray.include関数を型システムに実装する

type isPillarMen = Includes<['Kars', 'Esidisi', 'Wamuu', 'Santana'], 'Dio'> // expected to be `false`

回答

type Includes<T extends readonly any[], U> = T extends [infer L, ...infer R]
  ? [U, L] extends [L, U]
    ? true
    : Includes<R, U>
  : false

解説

  • T extends [infer L, ...infer R]で配列Tを分割し、先頭要素をL、残りの要素をRに取り出す(再帰処理の土台)
  • [U, L] extends [L, U]で、型ULと等しいかどうかを比較する
    タプルでまとめて両方向比較することで完全一致を確認できる(× U extends L
  • ⭐️続いて? true : Includes<R, U>で、ULに一致すればtrue、一致しなければ残りの要素Rに対して再帰的にIncludes<R, U>を呼ぶ
  • : false(ベースケース)で、配列Tが空になったら(つまり最後まで一致しなかった)らfalseを返す

🗣️ infer使いこなせる気がしない
🗣️ 再帰なんて思いつかない...

さとうさとう

Push

Array.pushのジェネリックバージョンを実装する

type Result = Push<[1, 2], '3'> // [1, 2, '3']

回答

type Push<T extends unknown[], U> = [...T, U]

解説

  • T extends unknown[]でジェネリック型Tは配列であることを指定
  • Uは追加する要素の型
  • ⭐️[...T, U]で、スプレッド構文を使ってTのすべての要素の後ろにUを追加した新しいタプルを生成

メモ

ジェネリック型

どんな型(T)でも受け取れるように作られた型
https://typescript-jp.gitbook.io/deep-dive/type-system/generics

さとうさとう

Unshift

Array.unshiftの型バージョンを実装する

type Result = Unshift<[1, 2], 0> // [0, 1, 2]

回答

type Unshift<T extends unknown[], U> = [U, ...T]

解説

  • T extends unknown[]でジェネリック型Tは配列であることを指定
  • Uは配列の先頭に追加したい要素の型
  • ⭐️[U, ...T]で、スプレッド構文を使ってUを先頭に置き、その後に配列型Tを追加した新しいタプルを生成
さとうさとう

Parameters

組み込みの型ユーティリティParameters<T>を使用せず、Tからタプル型を構築する型を実装する

const foo = (arg1: string, arg2: number): void => {}

type FunctionParamsType = MyParameters<typeof foo> // [arg1: string, arg2: number]

Parametersを使用すると以下

type Params = Parameters<typeof foo>  // [arg1: string, arg2: number]

回答

type MyParameters<T extends (...args: any[]) => any> =
    T extends (...any: infer S) => any ? S : any

解説

  • T extends (...args: any[]) => anyで、Tは「引数が任意の配列、戻り値は任意の関数型」であることを制限
  • ⭐️T extends (...any: infer S) => anyTの引数の型をSとして推論(infer)
  • ? S : anyは条件に合致すれば引数の型のタプルSを返し、そうでなければanyを返す
    → 関数の引数が複数ある場合、引数の型は順序付きの複数型の集合として「タプル型」で表現される
    → そのため引数の型をまとめて取り出すとタプル型として返される
このスクラップは1ヶ月前にクローズされました