Open7

TypeScript型パズルをやらないか?

むのう.devむのう.dev

モチベーション

TypeScripter()として、型パズルを通らずには一人前になれないので(過激派)

むのう.devむのう.dev

ユーティリティ型

まずはTypeScriptに標準で組み込まれているユーティリティ型を理解する

むのう.devむのう.dev

超定番

Required<T>, Partial<T>

Required<T>は全てのプロパティを必須にする

サンプル
type OptionalType = {
    value1?: string;
    value2?: string;
}

type RequiredType = Rquired<OptionalType>;
// ↓↓↓↓
type RequiredType = {
    value1: string;
    value2: string;
}

Partial<T>は全てのプロパティをオプショナルにする

サンプル
type RquiredType = {
    value1: string;
    value2: string;
}

type OptionalType = Partial<RquiredType>;
// ↓↓↓↓
type OptionalType = {
    value1?: string;
    value2?: string;
}

ReadOnly<T>

全てのプロパティを読み取り専用にする

サンプル
type Hoge = {
    value: string;
}

type ReadOnlyHoge = ReadOnly<Hoge>;
// ↓↓↓↓
type ReadOnlyHoge = {
    readonly value: string;
}
むのう.devむのう.dev

定番

Pick<T, Keys>

型Tのプロパティの内、Keysで指定したもののみを抽出した型を作る
型Tにないプロパティを指定するとコンパイルエラーになる

サンプル
type Hoge = {
    value1: string;
    value2: number;
}

type PickedHoge = Pick<Hoge, "value1">;
// ↓↓↓↓
type PickedHoge = {
    value1: string;
}

TODO: 型の追跡

Omit<T, Keys>

型Tのプロパティの内、Keysで指定したものを除いた型を作る
型Tにないプロパティを指定してもコンパイルエラーにならない

サンプル
type Hoge = {
    value1: string;
    value2: number;
}

type OmittedHoge = Omit<Hoge, "value1">;
// ↓↓↓↓
type PickedHoge = {
    value2: number;
}

Exclude<Union, Types>

ユニオン型から、指定した型を除いた型を作る
ユニオン型にない型を指定してもコンパイルエラーにならない
元の型のプロパティ名を変更、プロパティを追加した場合は、Exclude側も追従して変更する必要がある

サンプル
type UnionType = "A" | "B" | "C" | "D";
type ExcludedType = Exclued<UnionType, "C" | "D">
// ↓↓↓↓
type ExcludedType = "A" | "B";

Extract<Union, Types>

ユニオン型から指定した型のみを抽出した型を作る

サンプル
type UnionType = "A" | "B" | "C" | "D";
type ExtractedType = Extracted<UnionType, "C" | "D">
// ↓↓↓↓
type Extracted = "C" | "D";
むのう.devむのう.dev

使ったことない

NoInfer

端的にいうと型推論を制限するユーティリティ型
ジェネリクスTの推論材料となるものに対して使用すると、それを推論材料から外せる

サンプル
const indexOf = <T extends string>(array: T[], searchElement: T) => {
  return array.indexOf(searchElement);
};

とすると、searchElementもTを推論するための材料なので

indexOf(["ruby", "javascript"], "rust");

みたいな呼び出しができてしまう。

const indexOf = <T extends string>(array: T[], searchElement: T) => {
  return array.indexOf(searchElement);
};

にすれば型エラーになる

むのう.devむのう.dev

type-challenges

  • エディタのエラーメッセージに対応するように修正する
  • keyof Tで型のプロパティのユニオンを取得
  • T[P]でそのプロパティの型にアクセス
  • タプル、配列はT[number]で要素にアクセスできる。Input["name"]とかと同じノリ
  • 型でやる方が難易度高いので値でやってみる

Pick

ユーティリティ型のPickと同じ挙動をする型を自作

必要な知識

  • keyof
    • 型に対して使用する。その型のプロパティ名を文字列リテラル型 or ユニオン型で取得
    • type Hoge = {
          id: string;
          name: string;
      }
      type Keys = keyof Hoge;
      // Keys = "id" | "name";
      
  • in
    • keyofと組み合わせて使用(mapped type)。keyofと取得した文字列リテラル型 or ユニオン型を展開するような感じ
    • type FullName = {
          firstName: string;
          lastName: string;
      }
      type PatialFullName = {
          [P in keyof FullName]?: string;
      }
      // type PatialFullName = {
      //    firstName?: string | undefined;
      //    lastName?: string | undefined;
      // }      
      
  • プロパティの型情報へのアクセス
    • FullName["firstName"]みたいなやつ
回答
// ↓問題文
type MyPick<T, K> = any


// ①まずKがTのプロパティ名のいずれかな必要があるので
type MyPick<T, K extends keyof T> = any;

// ②与えられたKをプロパティとして**展開**する
type MyPick<T, K extends keyof T> = { [P in K]: any };

// ③プロパティの型をTの同じプロパティ名の型と合わせる
type MyPick<T, K extends keyof T> = { [P in K]: T[P] };

TupleToObject

与えられたタプルの要素をkey/valueにもつオブジェクトに変換

const tuple = ["a", 0, "c"];
// ↓
type TupleObject = {
    "a": "a",
    0: 0,
    "c": "c",
};

必要な知識

  • タプルの要素の型情報へのアクセスの仕方
    • HogeTuple[number]でタプルの要素の型をユニオン型で取得できる
    • イメージ的にはオブジェクトに対するkeyofと同じ
      • keyof { a: string; b: string; } = "a" | "b";
      • ["a", "b"][number] = "a" | "b";
回答
// ↓問題文
type TupleToObject<T extends readonly any[]> = any


// ①プロパティ名を与えられたタプルから取得する
type TupleToObject<T extends readonly any[]> = { [P in T[number]]: any };

// ②プロパティの型をプロパティ名と同じにする
type TupleToObject<T extends readonly any[]> = { [P in T[number]]: P };

Promise

Promise<T>型のTの型を取得する型を作成
同じ挙動のユーティリティ型でAwaited<T>というのもある

必要な知識

  • PromiseLike<T>
    • TypeScriptのビルトイン型。JavaScriptはthen関数を持っていればPromiseと判断され、PromiseLike型を使うと楽に実装できる
  • Conditional type
    • 条件型。通常の条件演算子と同じような文法で使える
      • T extends string ? T : never
  • infer
    • Conditional typeと共に使用する。型宣言時には固定したくないが、実際にinferを含む型を使うときに動的に型を指定したい場合に使う。ジェネリクス自体は式の左側で宣言して、型を使用する場合に与えられるが、infer付きの型は式の右側で宣言して右側で使用する。つまり、型を使用する時に明示的に何型が与えるのではなく、型使用時に与えたから推論させたい場合に便利
回答
// 問題文
type MyAwaited<T> = any;

// ①TがPromiseであることを保証する
type MyAwaited<T extends PromiseLike<any>> = any;

// ②conditional typesとinferを使用して、PromiseLikeのジェネリクス部分の型を取得
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U> ? U : never;

// ③このままだと、Promise<Promise<string>>などに対応できないので再帰処理する
//  PromiseLike<infer U>のUがPromiseLikeだった場合は、再度MyAwaitedを通す
type MyAwaited<T extends PromiseLike<any>> = T extends PromiseLike<infer U> ? U extends Promise<any> ? MyAwaited<U> : U : never;
むのう.devむのう.dev

まとめ

  • type-challgensをやること自体がtypescriptのutitlity型を知ることにつながる
  • ライブラリの型定義ファイルを見に行った時にギョッとすることが減る