🏋️

TypeScriptチョットワカルからチョットデキルになる

2023/04/21に公開

概要

プロジェクトでTypeScript触ってるから引数やpropsに型を定義するくらいはできる「TypeScriptチョットワカル」レベルから、 ある程度使いこなせてるなと思ってもらえる「TypeScriptチョットデキル」レベルになるためのTipsをまとめてみました。

対象読者

  • なんとなくTypeScriptを使っている
  • 難しいTypeScriptのコードが何をしてるか全然わからん
  • もっと実践的なTypeScriptを身につけたい

という方の参考になればと思います。

Generics

TypeScriptを使用するにあたり、まず初めにつまずくであろうGenericsから紹介します。

Genericsを使用すると型を引数として受け取ることができます。

受け取る引数の型を呼び出し時に指定できるため、汎用的な関数やコンポーネントを作成することができます。

特に、ライブラリの中身を見て挙動を追いたいときなど必ず出会うと思うので確実にマスターしておきたいところです。

サンプル

以下は、任意の型の配列から、null, undefinedを除外した配列を返す関数のサンプルです。

excludeNullAndUndefined.ts
const excludeNullAndUndefined = <T>(array: (T | null | undefined)[]): T[] => {
  return arr.filter((value): value is T => value !== null && value !== undefined);
}

<T>の部分がGenerics宣言で、任意の型を受け取れることを示しています。
(慣習的にTypeを意味するTが用いられることが多いですが、任意の名前をつけることができるのでプロジェクトのルールに従ってください。)

arrayの型注釈で再びTが出てきます。
これは<T>で受け取った型を使用することを示しています。
(受け取った型がstringであれば(string | null | undefined)[]になるイメージ)

value is Tもあまり見慣れないかもしれませんが、これはfilerの戻り値が推論されないためvalueT型であることをTypeScriptに伝えるための記述(型ガード)です。

以下が、実際にexcludeNullAndUndefinedを使用したサンプルです。

const nullableStringArray = ['a', null, 'b', undefined, 'c'];
const nullableNumberArray = [1, null, 2, undefined, 3];
const nullableStringOrNumberArray = [1, null, 'a', undefined, 2];

const nonNullableStringArray = excludeNullAndUndefined(nullableStringArray); // string[]
const nonNullableNumberArray = excludeNullAndUndefined(nullableNumberArray); // number[]
const nonNullableStringOrNumberArray = excludeNullAndUndefined(nullableStringOrNumberArray); // (string | number)[]

console.log(nonNullableStringArray); // ['a', 'b', 'c']
console.log(nonNullableNumberArray); // [1, 2, 3]
console.log(nonNullableStringOrNumberArray); // [1, 'a', 2]

Satisfies

わりと最近のTypeScript 4.9から追加された便利な機能Satisfiesについて紹介します。

Satisfiesは式の型が条件を満たすかどうかを判定します。

言葉では全然分からないのでサンプルを見ていきましょう。

サンプル

例えば以下のようなページ情報を管理するオブジェクトがあったとします。

export const PAGES = {
  home: {
    children: <Home />,
    path: '/',
    seo: {
      title: 'TS TIPS | Home',
    },
  },
  generics: {
    children: <GenericsPage />,
    path: '/generics',
    seo: {
      title: 'TS TIPS | Generics',
    },
  },
} as const

このオブジェクトにアクセスする際にPAGES.と入力するとhome, genericsの候補が表示されます。

const hoge = PAGES. // <- home, generics

しかし、PAGESオブジェクトは型定義されていないため、新たなページ情報を追加しようとした際に以下のようになる可能性があります。

export const PAGES = {
  // ...
  satisfies: {
    children: <SatisfiesPage />,
    path: '/satisfies',
    title: 'TS TIPS | Satisfies', // seoではなくtitleとして定義してもエラーにはならない
  },
} as const

これを防ぐためにPAGESに型注釈をつけてあげるという方法を思いつくかもしれません。

type PageObjectType = {
  children: ReactNode
  path: string
  seo: {
    title: string
  }
}

export const PAGES: Record<string, PageObjectType> = {
  // ...
  satisfies: {
    children: <SatisfiesPage />,
    path: '/satisfies',
    title: 'TS TIPS | Satisfies', // error!
  },
} as const

これで万事解決に思えるかもしれませんが、型注釈をつけると型チェックができる代わりに型推論が効かなくなってしまい、PAGES.と入力しても入力候補が出てこなくなります。

const hoge = PAGES. // <- ここで入力候補が出てこない

そこで活躍するのがSatisfiesです!

export const PAGES = {
  // ...
} as const satisfies Record<string, PageObjectType>

型注釈を削除してオブジェクトの最後にsatisfes Typeとすることで、PAGESの型チェックをしながらもPAGES.で型推論が効くようになります。

export const PAGES = {
  // ...
  satisfies: {
    children: <SatisfiesPage />,
    path: '/satisfies',
    title: 'TS TIPS | Satisfies', // error!
  },
} as const satisfies Record<string, PageObjectType>

const hoge = PAGES. // <- home, generics, satisfies 🎉

Conditional Types

Conditional Types(条件付きの型)は型の条件分岐ができる機能です。

サンプル

ここではConditional Typesを使用して、TypeScriptのUtility typesであるNonNullableを自作することで理解を深めていきます。

NonNullableNonNullable<Type>という形で使用し、Typeからnullundefinedを除外した型を返します。

以下がNonullableの実装を模倣したサンプルコードです。

type MyNonNullable<T> = T extends null | undefined ? never : T;

Tnullまたはuendefinedだった場合にneverを返し、それ以外の場合はTを返します。

条件 ? 真 : 偽という形なので三項演算子をイメージすると理解しやすいかと思います。

type NullableString = string | null | undefined;

type NonNullableString = MyNonNullable<NullableString>; // string
実際のNonNullableの定義

実際にNonNullableの中身を見てみると以下のように定義されています。

type NonNullable<T> = T & {}

これはT型に型を持たない空のオブジェクトを結合することでnullundefinedを除外するというシンプルな方法で成り立っています。

Infer

実務で使うことはほとんどない気がしますが、ライブラリでは使わているところを見かけるので読めるようになることを目的に紹介します。

Inferは「推論」を意味し、Genericsから特定の型を抽出するのに使用されます。

また、先程のConditional Typesとセットで使われることが多いので一緒に覚えておきましょう。

サンプル

ここでは、TypeScriptのUtility TypesであるReturnTypeの中身を見ながら理解を深めていきます。

ReturnType関数の戻り値を抽出する際に使用します。

実際のReturnTypeの定義は以下のようになっています。

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

初見では何が起こってるかよくわからないと思いますので、噛み砕きながら見ていきます。

  1. <T extends (...args: any) => any>の部分で、any型を引数にとりany型を返す関数の型をTとして受け取る
  2. T extends (...args: any) => infer Rの部分(Conditional Types)で、Tが1の条件を満たす関数の型である場合にRを推論(キャプチャ)し返却
  3. 条件が満たされない場合はanyを返却

という流れになっています。

以下のようなnumber型を2つ引数に受け取りnumber型を返す関数addReturnTypeに渡した際の中身の流れを追いながらより具体的に見ていきます。

const add = (a: number, b: number) => a + b

type AddReturnType = ReturnType<typeof add> // number
  1. ReturnTypeGenericsとして(a: number, b: number) => number型の関数を受け取る
  2. 受け取った関数がConditional Typesの条件にマッチするため、戻り値であるnumberRにキャプチャされ返却

仮にstring型のような(...args: any) => anyの条件にマッチしない型を渡した場合はanyが返却されます。

type StringReturnType = ReturnType<string> // any

Mapped Types

最後にMapped Typesについて紹介します。

Mapped Typesオブジェクトの型のプロパティを変換する際に使用します。

Utility TypesのPartialReadonlyなどがMapped Typesを使用して実装されています。

Partialの定義
type Partial<T> = {
  [P in keyof T]?: T[P];
};
Readonlyの定義
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

サンプル

例えば以下のような商品のモデルを表す型があるとします。

type ProductType = {
  name: string,
  price: number
  createdAt: Date,
}

このモデルの型を元にAPIレスポンスの型を生成したい時にMapped Typesが役立ちます。

// カスタムエラーの型
type CustomErrorType = { code: number, message: string }

// Modelの型をもとにResponseの型情報を生成するMapped Types
type ResponseType<T> = {
  [Property in keyof T]: T[Property] | CustomErrorType;
}
// Product ModelのResponse型を生成
type ProductResponseType = ResponseType<ProductType>

Mappted Typesを使用しているResponseTypeは以下のような処理を行っています。

  1. 受け取ったT型のプロパティ(key部分)をkeyof Tで取得
  2. T[Property]でプロパティの型(value部分)を取得
  3. 2で取得した型にUnion TypeとしてCustomErrorTypeを追加(T[Property] | CustomErrorType

この時のProductResponseTypeの型情報は以下のようになります。

type ProductResponseType = {
  name: string | CustomErrorType,
  price: number | CustomErrorType,
  createdAt: Date | CustomErrorType,
}

まとめ

  • Generics型を引数として受け取れる
  • Satisfies型が条件を満たすかどうかを判定する際に使用する
  • Conditional Types条件によって型を変更する際に使用する
  • InferGenericsから特定の型を抽出するのに使用される
  • Mapped Typesオブジェクトの型のプロパティを変換する際に使用する

よいType Safe Lifeを👋

参考文献

Discussion