KENTEM TechBlog

建設業のDXを実現するKENTEMの技術ブログです。

TypeScript型定義!便利だと感じた実践テクニック集

こんにちは!新卒フロントエンドエンジニアのK.Sです。
皆さんは既にこちらの記事をご覧になりましたか?

tech.kentem.jp

この記事で紹介されているユーティリティ型だけでも十分役に立ちますが、別のテクニックと組み合わせることでさらに柔軟な型の定義が可能になります。
この記事では、私が便利だと感じた実践的に使えるテクニックをご紹介します。

基本となる型定義(共通型)

この記事で使用する共通の型を定義します。

type User = {
  id: number;
  name: string;
  email: string;
}

Omit × Pick × Partial】 一部のプロパティだけをオプショナルにする

一部だけ省略可能にするときに使用します。
以下はemailのみをオプショナルにする例です。PATCH APIだとemailが省略可能な場合などに便利です。

type OptionalEmail = Omit<User, 'email'> & Partial<Pick<User, 'email'>>
// => {
//   id: number;
//   name: string;
//   email?: string;
// }

satisfies × Record × as const】型と値の両面で安全なマップを作る

型安全な定数マップを作りたいときに使用します。
例えば、以下のようにフォーム項目の日本語ラベルを定義したい場合などに便利です。

const userLabels = {
  id: 'ユーザーID',
  name: '名前',
  email: 'メールアドレス',
} as const satisfies Record<keyof User, string>;
// => { 
//   readonly id: "ユーザーID";
//   readonly name: "名前";
//   readonly email: "メールアドレス";
// }

Record × Exclude】 既存のユニオン型から、特定のものだけ除外したマッピングを作る

ユニオン型から不要な型を除いた設定を作りたい場合に便利です。

type UserKeyIds = 'id' | 'name' | 'email';
type UserConfig = Record<Exclude<UserKeyIds, 'email'>, string>
// => {
//   id: string;
//   name: string;
// }
【Record × keyof × Omit】との使い分け

keyof -> Exclude -> RecordOmit -> keyof -> Recordは、どちらも不要なキーを除外してマッピングを作成する便利なテクニックですが、
得られる結果は同じです。
しかし、元となるオブジェクトがネストの深い複雑なオブジェクト型のとき、
keyof -> Exclude -> Recordの方がパフォーマンスが向上する可能性があります。

例えば以下のようなオブジェクト型があったとします。

interface ComplexUser {
  id: number;
  name: string;
  email: string;
  profile: {
    avatar: string;
    bio: string;
    socialLinks: {
      twitter?: string;
      github?: string;
      linkedin?: string;
    };
    preferences: {
      theme: 'light' | 'dark';
      notifications: {
        email: boolean;
        push: boolean;
        sms: boolean;
      };
      privacy: {
        profilePublic: boolean;
        showEmail: boolean;
      };
    };
  };
  permissions: {
    canEdit: boolean;
    canDelete: boolean;
    canShare: boolean;
  };
  metadata: {
    createdAt: Date;
    updatedAt: Date;
    lastLogin: Date;
    loginCount: number;
  };
}

以下のように結果は同じですが、Omitはネストした型構造全体をコピーして再構築するため重い処理となってしまいます。それに対し、keyof -> Excludeではオブジェクトの構造を解析せず、キー名のみを抽出し、ユニオン型操作を行うため比較的型計算が軽くなります。

// keyof -> Exclude -> Record
type ComplexUserLabelsMultiExclude = Record<
  Exclude<keyof ComplexUser, 'email' | 'metadata' | 'permissions'>,
  string
>;
// Omit -> keyof -> Record
type ComplexUserLabelsMultiOmit = Record<
  keyof Omit<ComplexUser, 'email' | 'metadata' | 'permissions'>,
  string
>;
// どちらも結果は同じ
// =>{
//   id: string;
//   name: string;
//   profile: string;
// }

ComponentProps】 コンポーネントからpropsの型を抽出する

ComponentProps は、React コンポーネントから props の型を抽出できるユーティリティ型です。
これは、再利用性を上げたいときや、ラッパーコンポーネントを作成するときに便利です。

type MyButtonProps = {
  label: string
  onClick: () => void
}

const MyButton = ({ label, onClick }: MyButtonProps) => {
  return <button onClick={onClick}>{label}</button>
}

type Props = ComponentProps<typeof MyButton>
// => { label: string; onClick: () => void }

【余談】 その他のテクニック

使用場所は限られますが、知っておくとためになるテクニックをご紹介します。

extends / keyof 型の制約

extends は、型引数に条件や制約を付けるために使用します。
例えば、useState でオブジェクトを管理する場合に、プロパティ名を制約付きで指定できる関数を作ると、 存在しないキーを更新しようとしたときにコンパイル時にエラーが出るため、安全に更新できます。

const [user, setUser] = useState<User>({ id: 1, name: 'Alice', email: 'a@example.com' })

function updateUser<K extends keyof User>(key: K, value: User[K]) {
  setUser(prev => ({ ...prev, [key]: value }))
}

updateUser('name', 'Bob')   // OK
updateUser('password', 'x') // エラー: Userに存在しないキー
Mapped Types 型のマッピング

実際に使用する場面は少ないと思いますが、ユーティリティ型が内部で型をどう生成しているかを理解するために役立ちます。

type ToString<T> = {
  [K in keyof T]: string
}

type StringifiedUser = ToString<User>
// => { id: string; name: string; email: string }
Recursive Types 再帰型

こちらも使用する場面はないですが、知っておくと良いかもしれません。
ネストされた深い構造のオブジェクトの階層すべてをReadonlyにするDeepReadonlyを自作する際に使用されることが多いようです。

type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K]
}

type NestedUser = { info: { age: number } }
const n: DeepReadonly<NestedUser> = { info: { age: 10 } }
n.info.age = 20 // エラー
infer 推論

条件付き型(extends)の中で型を推論するときに使う特殊構文です。
こちらも使用することは少ないですが、ReturnTypeなどのユーティリティ型内部で使用されています。

type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never

type Fn = () => Promise<string>
type Result = GetReturnType<Fn>
// => Promise<string>

まとめ

この記事では、型定義のテクニックについてまとめました。 今回紹介したテクニックを使うことで、さらに効率的な型の定義ができます。ぜひ活用してみてください。
ここまで読んでいただきありがとうございました!
(いくつか使いどころが難しいものもありますが、使いこなせるとかっこいいですね🐱)

おわりに

KENTEMでは、様々な拠点でエンジニアを大募集しています! 建設×ITにご興味頂いた方は、是非下記のリンクからご応募ください。 recruit.kentem.jp career.kentem.jp