TypeScriptチョットワカルからチョットデキルになる
概要
プロジェクトでTypeScript触ってるから引数やpropsに型を定義するくらいはできる「TypeScriptチョットワカル」レベルから、 ある程度使いこなせてるなと思ってもらえる「TypeScriptチョットデキル」レベルになるためのTipsをまとめてみました。
対象読者
- なんとなくTypeScriptを使っている
- 難しいTypeScriptのコードが何をしてるか全然わからん
- もっと実践的なTypeScriptを身につけたい
という方の参考になればと思います。
Generics
TypeScriptを使用するにあたり、まず初めにつまずくであろうGenerics
から紹介します。
Generics
を使用すると型を引数として受け取ることができます。
受け取る引数の型を呼び出し時に指定できるため、汎用的な関数やコンポーネントを作成することができます。
特に、ライブラリの中身を見て挙動を追いたいときなど必ず出会うと思うので確実にマスターしておきたいところです。
サンプル
以下は、任意の型の配列から、null
, undefined
を除外した配列を返す関数のサンプルです。
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
の戻り値が推論されないためvalue
がT
型であることを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
を自作することで理解を深めていきます。
NonNullable
はNonNullable<Type>
という形で使用し、Type
からnull
とundefined
を除外した型を返します。
以下がNonullable
の実装を模倣したサンプルコードです。
type MyNonNullable<T> = T extends null | undefined ? never : T;
T
がnull
またはuendefined
だった場合にnever
を返し、それ以外の場合はT
を返します。
条件 ? 真 : 偽
という形なので三項演算子をイメージすると理解しやすいかと思います。
type NullableString = string | null | undefined;
type NonNullableString = MyNonNullable<NullableString>; // string
実際のNonNullableの定義
実際にNonNullable
の中身を見てみると以下のように定義されています。
type NonNullable<T> = T & {}
これはT
型に型を持たない空のオブジェクトを結合することでnull
とundefined
を除外するというシンプルな方法で成り立っています。
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;
初見では何が起こってるかよくわからないと思いますので、噛み砕きながら見ていきます。
-
<T extends (...args: any) => any>
の部分で、any
型を引数にとりany
型を返す関数の型をT
として受け取る -
T extends (...args: any) => infer R
の部分(Conditional Types
)で、T
が1の条件を満たす関数の型である場合にR
を推論(キャプチャ)し返却 - 条件が満たされない場合は
any
を返却
という流れになっています。
以下のようなnumber
型を2つ引数に受け取りnumber
型を返す関数add
をReturnType
に渡した際の中身の流れを追いながらより具体的に見ていきます。
const add = (a: number, b: number) => a + b
type AddReturnType = ReturnType<typeof add> // number
-
ReturnType
のGenerics
として(a: number, b: number) => number
型の関数を受け取る - 受け取った関数が
Conditional Types
の条件にマッチするため、戻り値であるnumber
がR
にキャプチャされ返却
仮にstring
型のような(...args: any) => any
の条件にマッチしない型を渡した場合はany
が返却されます。
type StringReturnType = ReturnType<string> // any
Mapped Types
最後にMapped Types
について紹介します。
Mapped Types
はオブジェクトの型のプロパティを変換する際に使用します。
Utility TypesのPartial
やReadonly
などが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
は以下のような処理を行っています。
- 受け取った
T
型のプロパティ(key部分)をkeyof T
で取得 -
T[Property]
でプロパティの型(value部分)を取得 - 2で取得した型に
Union Type
としてCustomErrorType
を追加(T[Property] | CustomErrorType
)
この時のProductResponseType
の型情報は以下のようになります。
type ProductResponseType = {
name: string | CustomErrorType,
price: number | CustomErrorType,
createdAt: Date | CustomErrorType,
}
まとめ
-
Generics
は型を引数として受け取れる -
Satisfies
は型が条件を満たすかどうかを判定する際に使用する -
Conditional Types
は条件によって型を変更する際に使用する -
Infer
はGenerics
から特定の型を抽出するのに使用される -
Mapped Types
はオブジェクトの型のプロパティを変換する際に使用する
よいType Safe Lifeを👋
Discussion