Open12

TypeScriptの便利な型TIPS

softoikasoftoika

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); // ビルドエラーになる
  }
}
softoikasoftoika

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
softoikasoftoika

少し型パズルすれば['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'

詳しくはこちら
https://qiita.com/FuwattoFlower/items/66c315c29587417a502e

softoikasoftoika

型のインデックスアクセスで型定義を正規化する

例えば次のようなidの型定義はidの型を変更する時が大変です。

interface User {
  id: number;
  ...
}
function getUser(userId: number) {}

もしユーザーIDをstringにしたくなったら変更漏れがないかの確認は結構大変だと思います。
変更の可能性がなくとも同じものの定義は一箇所に統一したいです。
こういう場合は次のように書くといいです。

function getUser(userId: User['id']) {}

こうするメリットはこのプロパティに対する説明(コメント)を元の型定義一箇所に書けばいいという点にもあります。

interface User {
  /**
    * 長々とした説明
    * ...
    */
  id: number;
  ...
}
softoikasoftoika

Tuple型をUnion型に変換する

type TupleToUnion<T extends any[]> = T[number];
softoikasoftoika

交差型の見た目を整える

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] }>;
softoikasoftoika

数値リテラルのユニオン型を配列の長さだけ生成する

これを使うと色々と悪さできることもある。

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くらいまで。

softoikasoftoika

TypeScript3.8で追加されたType-Only Importsを使う

TypeScript3.8以降はこのようにして型だけimportできる[1]

import type { Foo }  from "./foo";

こうすることで型だけ必要な時は、実質ランタイム上はないものとして扱われるためバンドルサイズに優しい(のだと思う)。

ただ結構これ使うの忘れがちだよね、ってことで便利なのがconsistent-type-importsというeslintルール[2]
型をimportしてるけどtype-only importsになってないものを検知してくれる。

.eslintrc.json
{
  "rules": {
    "@typescript-eslint/consistent-type-imports": "error"
  }
}
softoikasoftoika

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"] };

参考

https://github.com/microsoft/TypeScript/issues/1778

softoikasoftoika

特定のプロパティのみ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]
};
softoikasoftoika

関数の型から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