跳到主要内容
稻草人
プロフィール

EP.02

💻Technology

TypeScript Utility Typesを実務で使うメモ

Partial、Pick、OmitなどのUtility Typesを実務でどう使うかのメモ。

BY 稻, 草人

TypeScriptのUtility Typesは便利ですが、名前だけ覚えても実務では使いどころに迷います。自分も最初はPartialをよく使い、エラーが消えたことで安心していました。しかし、必要な項目まで任意になり、別の場所で判定が増えることがありました。

Utility Typesは型を短くする道具というより、既存の型との関係を明確にする道具だと考えると分かりやすいです。

目次

基準になる型

type User = {
  id: number
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
  active: boolean
  createdAt: string
  updatedAt: string
}

Pick:必要な項目だけ選ぶ

一覧表示では全項目が不要です。

type UserListItem = Pick<
  User,
  'id' | 'name' | 'role' | 'active'
>

元のUserでroleの型が変わればこちらにも反映されます。似た型を手書きで複製するより、関係が明確です。

Omit:一部を除外する

登録時はIDや更新日時を送りません。

type CreateUserInput = Omit<
  User,
  'id' | 'createdAt' | 'updatedAt'
>

ただし、API入力が将来独自に変わるなら、無理にエンティティから作らず専用型を定義したほうが読みやすい場合もあります。

Partial:全部を任意にする

部分更新には便利です。

type UpdateUserInput = Partial<
  Pick<User, 'name' | 'email' | 'role' | 'active'>
>

Partial<User>にするとidや作成日時まで更新対象に見えるため、先にPickで範囲を限定します。

「フォーム入力途中だから」と完成データ全体をPartialにすると、利用側でundefined判定が増えます。入力途中専用の型を作る選択肢もあります。

Required:任意項目を必須にする

type DraftOptions = {
  title?: string
  category?: string
}

type ValidatedOptions = Required<DraftOptions>

設定の読込直後は任意、初期値を補った後は必須、という段階を表すときに使えます。

Readonly:変更させない

type CurrentUser = Readonly<
  Pick<User, 'id' | 'name' | 'role'>
>

これは実行時の完全な不変を保証するものではありませんが、意図しない代入をコンパイル時に防げます。配列ならReadonlyArray<T>も利用できます。

Record:キーと値を対応させる

type Role = User['role']

const roleLabels: Record<Role, string> = {
  admin: '管理者',
  editor: '編集者',
  viewer: '閲覧者'
}

Roleへ新しい値を追加したとき、ラベルの追加漏れを検出できます。Record<string, string>ではその利点が弱くなるため、キーを具体的なunionにします。

ExcludeとExtract

type EditableRole = Exclude<Role, 'admin'>
type ManagementRole = Extract<Role, 'admin' | 'editor'>

既存unionから値を除外・抽出できます。ただし複雑に組み合わせると読みづらくなるため、業務上の意味が分かる型名を付けます。

NonNullable

type SelectedUser = User | null | undefined
type ConfirmedUser = NonNullable<SelectedUser>

検証後の値や関数の戻り値を表すときに便利です。

ReturnTypeとAwaited

async function fetchUsers() {
  return [] as User[]
}

type FetchUsersResult =
  Awaited<ReturnType<typeof fetchUsers>>

関数の実装と型を連動できます。ただし公開APIの重要な型は、関数から推論するより名前を付けて明示したほうが仕様として読みやすいこともあります。

satisfiesも便利

Utility Typeではありませんが、定義した型を満たしているか確認しつつ、具体的な値の推論を保てます。

const permissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read']
} satisfies Record<Role, string[]>

使いすぎない

type Input = Partial<
  Omit<
    Pick<User, 'id' | 'name' | 'email' | 'role'>,
    'id'
  >
>

このように重ねると、完成形が分かりにくくなります。型が業務上の重要な概念なら、専用型を直接書くほうがよいです。

判断基準は「短いか」ではなく、「変更理由と用途が伝わるか」です。

チェックリスト

  • Partialで本当に全項目を任意にしてよいか
  • Pickで対象範囲を限定できないか
  • エンティティとAPI入力を無理に連動させていないか
  • Recordのキーが具体的なunionか
  • Utility Typesの重ねすぎで読みにくくなっていないか
  • 独立した業務概念には名前付き型を用意したか

まとめ

Utility Typesは重複を減らすだけでなく、「この型は元の型のどこから作られたか」を表現できます。一方で、すべてを一行に圧縮すると意図が見えなくなります。

まず専用型を素直に書き、関係が明確な場所でPick、Omit、Partial、Recordを使うくらいが、保守しやすいバランスだと感じています。

参考資料