
こんにちは!新卒フロントエンドエンジニアのK.Sです。
皆さんは既にこちらの記事をご覧になりましたか?
この記事で紹介されているユーティリティ型だけでも十分役に立ちますが、別のテクニックと組み合わせることでさらに柔軟な型の定義が可能になります。
この記事では、私が便利だと感じた実践的に使えるテクニックをご紹介します。
- 基本となる型定義(共通型)
- 【Omit × Pick × Partial】 一部のプロパティだけをオプショナルにする
- 【satisfies × Record × as const】型と値の両面で安全なマップを作る
- 【Record × Exclude】 既存のユニオン型から、特定のものだけ除外したマッピングを作る
- 【ComponentProps】 コンポーネントからpropsの型を抽出する
- 【余談】 その他のテクニック
- まとめ
- おわりに
基本となる型定義(共通型)
この記事で使用する共通の型を定義します。
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 -> RecordとOmit -> 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