TypeScriptの便利な型TIPS
記事にするまでもない小ネタ集
never型による網羅性チェック
以下の関数をenum/Union型に対するswitch文で使うと網羅性のチェックがビルド時にできる
function assertNever(value: never, message?: string): never {
throw new Error(message ?? `Illegal value: ${value}`);
}
使用例 Playground
enum Fruit {
Apple,
Orange,
Grape
}
function eatFruit(fruit: Fruit): string {
switch(fruit) {
case Fruit.Apple:
return 'eat apple';
case Fruit.Orange:
return 'eat orange';
// case Fruit.Grape:
// return 'eat grape';
default:
assertNever(fruit); // ビルドエラーになる
}
}
enumライクなオブジェクトを定義する
enumはTypeScriptのシンタックスなのでJSに変換すると、特にバリエーションの多いenumは変換後のコードが膨れ上がってしまいます。
var Fruit;
(function (Fruit) {
Fruit[Fruit["Apple"] = 0] = "Apple";
Fruit[Fruit["Orange"] = 1] = "Orange";
Fruit[Fruit["Grape"] = 2] = "Grape";
})(Fruit || (Fruit = {}));
TypeScript上の記述量は増えてしまいますが、次のようにすると純粋なオブジェクトでenumと同等のものが書けます。
const Fruit = { Apple: 0, Orange: 1, Grape: 2 } as const;
type Fruit = typeof Fruit[keyof typeof Fruit]; // 0 | 1 | 2
少し型パズルすれば['Apple', 'Orange', 'Grape']
のようなタプルを与えることで上記のオブジェクトを生成する関数を作ることができます。
const OperatingSystem = enumObject(['macOS', 'Windows', 'Linux'] as const);
type OperatingSystem = Values<typeof OperatingSystem>;
let os: OperatingSystem; // 'macOS' | 'Windows' | 'Linux'
os = OperatingSystem; // Error!
os = OperatingSystem.MacOS; // 'macOS'
詳しくはこちら
型のインデックスアクセスで型定義を正規化する
例えば次のようなidの型定義はidの型を変更する時が大変です。
interface User {
id: number;
...
}
function getUser(userId: number) {}
もしユーザーIDをstringにしたくなったら変更漏れがないかの確認は結構大変だと思います。
変更の可能性がなくとも同じものの定義は一箇所に統一したいです。
こういう場合は次のように書くといいです。
function getUser(userId: User['id']) {}
こうするメリットはこのプロパティに対する説明(コメント)を元の型定義一箇所に書けばいいという点にもあります。
interface User {
/**
* 長々とした説明
* ...
*/
id: number;
...
}
Tuple型をUnion型に変換する
type TupleToUnion<T extends any[]> = T[number];
交差型の見た目を整える
type Flatten<T> = { [P in keyof T]: T[P] };
type Foo = Flatten<{ a: number } & { b?: string}>;
// -> { a: number; b?: string | undefined }
任意のプロパティをoptionalにする型
type OptionalProps<T, U extends keyof T> =
Flatten<{ [P in U]?: T[P] } & { [P in Exclude<keyof T, U>]: T[P] }>;
任意のプロパティをreadonlyにする型
type ReadonlyProps<T, U extends keyof T> =
Flatten<{ readonly [P in U]: T[P] } & { [P in Exclude<keyof T, U>]: T[P] }>;
数値リテラルのユニオン型を配列の長さだけ生成する
これを使うと色々と悪さできることもある。
type NumberLiteral<T extends any[], U extends number = 0> =
T extends []
? U
: T extends [any, ...infer Rest]
? NumberLiteral<Rest, Rest["length"] | U>
: never;
type N = NumberLiteral<['a', 2, 'foo', false]>; // 0 | 1 | 2 | 3
TypeScriptの制約上、上限の長さは大体46くらいまで。
TypeScript3.8で追加されたType-Only Importsを使う
TypeScript3.8以降はこのようにして型だけimportできる[1]
import type { Foo } from "./foo";
こうすることで型だけ必要な時は、実質ランタイム上はないものとして扱われるためバンドルサイズに優しい(のだと思う)。
ただ結構これ使うの忘れがちだよね、ってことで便利なのがconsistent-type-imports
というeslintルール[2]
型をimportしてるけどtype-only importsになってないものを検知してくれる。
{
"rules": {
"@typescript-eslint/consistent-type-imports": "error"
}
}
An index signature parameter type cannot be a type alias.エラーを回避する
インデックスシグネチャに型エイリアスを指定するとこのエラーが発生する。
例えば次のようなマップを作りたい場合
interface User {
id: number;
name: string;
}
type UserIdMap = { [id: User["id"]]: User["name"] }; // Error!
これはMapped Typesでおなじみのin
句を使うと回避できる。
type UserIdMap = { [id in User["id"]: User["name"] };
参考
特定のプロパティのみOptional/Required/Readonlyにするユーティリティ型
全てのプロパティに対してOptional/Required/Readonlyにするユーティリティ型は標準で用意されているが、特定のプロパティにのみ適用する型はMapped Typeを用いて定義しなければならない。
特定のプロパティのみOptionalにする型
type OptionalProps<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
} & {
[Q in K]?: T[Q]
};
type Example = OptionalProps<
{ foo: string; bar: number; baz: boolean },
"foo" | "bar"
>; // { baz: boolean } & { foo: string | undefined, bar: number | undefined }
これを応用して特定の型のみRequiredあるいはReadonlyにする型も作れる
type RequiredProps<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
} & {
[Q in K]-?: T[Q]
};
type ReadonlyProps<T, K extends keyof T> = {
[P in Exclude<keyof T, K>]: T[P];
} & {
readonly [Q in K]: T[Q]
};
関数の型からasync関数の型を求める
戻り値の型をPromiseでwrapした関数の型を求めたい。
例えば、(arg1: number, arg2: string) => boolean
を受け取ったら(arg1: number, args2: string) => Promise<boolean>
を返すような型を求めたい。
結論から言うと次のような型を定義すればよい。
export type Async<T extends (...args: any) => any> =
T extends (...args: infer P) => infer R
? (...args: P) => Promise<R>
: T;
関数から引数の型や戻り値の型を取得するのはParameters<Type>やReturnType<Type>が組み込みのユーティリティ型として用意されているのでその応用となる。
ちなみにParameters<Type>
、ReturnType<Type>
の実装は以下の通り
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P) => any ? P : never;
type ReturnType<T extends (...args: any) => any> =
T extends (...args: any) => infer R ? R : any