⚒️

TypeScript の便利な型, Tips まとめ

2022/12/27に公開

TypeScript の便利な型や Tips をまとめました。随時追加していきます。

再帰的に Readonly にする型DeepReadonly<T>

TypeScript にはReadonly<T>というユーティリティ型がありますがネストの深い object まではreadonlyになりません。
ネストが深い場合にも対応する場合は次のような型を自作する必要があります。

type DeepReadonly<T> = T extends Primitive | AnyFunction
  ? T
  : T extends Array<infer U>
  ? DRArray<U>
  : T extends Map<infer K, infer V>
  ? DRMap<K, V>
  : T extends Set<infer M>
  ? DRSet<M>
  : DRObject<T>;

type DRArray<T> = ReadonlyArray<DeepReadonly<T>>;
type DRMap<K, V> = ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>;
type DRSet<T> = ReadonlySet<DeepReadonly<T>>;
type DRObject<T> = { readonly [K in keyof T]: DeepReadonly<T[K]> };
type AnyFunction = (...args: any) => any;
type Primitive = undefined | null | boolean | string | number | symbol | bigint;

object のプロパティの値を union 型で得る型 ValueOf<T>

この型を使うことで enum のように object を 扱えます。

type ValueOf<T> = T[keyof T];

const ErrorMessage = {
  ConnectionFailed: "通信に失敗しました",
  AccessConcentration: "アクセスが集中しています",
} as const;

type ErrorMessage = ValueOf<typeof ErrorMessage>; // -> "通信に失敗しました" | "アクセスが集中しています"

object にメソッドが含まれている場合ValuePatternOf<T>

こちらはメソッドが含まれている場合に対応できます。

type AnyFunction = (...args: any) => any;
type ValuePatternOf<T> = ReturnType<Extract<T[keyof T], AnyFunction>> | Exclude<T[keyof T], AnyFunction>;

type ItemName = "アイテム1" | "アイテム2";
const ErrorMessage = {
  ConnectionFailed: "通信に失敗しました",
  ItemNotFound: (item: ItemName) => `${item}が見つかりません` as const,
} as const;

type ErrorMessage = ValuePatternOf<typeof ErrorMessage>; // -> "通信に失敗しました" | "アイテム1が見つかりません" | "アイテム2が見つかりません"

タプルや配列の型を返す

// タプル
const Hoge = ["hoge", "fuga", 10] as const;
type Hoge = typeof Hoge[number]; // -> "hoge" | "fuga" | 10

//配列
type Fuga = string[][number]; // -> string

filter で型の絞り込みをする

filter メソッドは union 型を絞り込むことができません。
isを使用して次のように書くことで絞り込めます。

// el の型が変わっても対応できる
const hoge = array.filter((el): el is Exclude<typeof el, undefined> => !!el);

// null/undefined を除きたい場合
const fuga = array.filter(<T>(el: T): el is NonNullable<T> => !!el);

mapped types や index signatures で追加のプロパティを定義する

const ids = ["id1", "id2", "id3", "id4"] as const;
type Id = typeof ids[number];

type ItemDetail = {
  [key in Id]: {
    name: string;
    sort: number;
  };
  // itemFormLabel: string; -> ここに追加するとエラーになる
} & {
  itemFormLabel: string; // 交差型を使うことで定義できる
};

Exhaustiveness checking(網羅性チェック)

never 型の never 型以外を代入できない特性を利用することで、パターンが網羅されていない場合にエラーを出すことができます。

以下のコードはcase "triangle":のケースを追加し忘れています。default 節のshapeTriangle型と推論され、never 型に代入しようとしているためにエラーとなります。
case "triangle":のケースがあればshapeは never 型となり、エラーは起きません。

interface Circle {
  kind: "circle";
  radius: number;
}
interface Square {
  kind: "square";
  sideLength: number;
}
interface Triangle {
  kind: "triangle";
  sideLength: number;
}
type Shape = Circle | Square | Triangle;
 
function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      const _exhaustiveCheck: never = shape; // ここでエラー
      return _exhaustiveCheck;
  }
}

以下は静的エラーに加えて実行時エラーも発生させる書き方です。
バグなどで型と実態が合わなくなったとき、実行時エラーが発生することで気づくことができます。

function getArea(shape: Shape) {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.sideLength ** 2;
    default:
      throw new Error(`Unknown type: ${(shape as { kind: "__invalid__" }).kind}`);
  }
}

参考:TypeScriptのexhaustiveness checkをスマートに書く

Discussion