はじめに
TypeScript では、 Mapped Type や Conditional Typesを利用することで、既存の interface から様々な型を定義することができます。
この記事では、以下の Person
という interface を基にして、新たな型定義を行う例を紹介していきます。
interface Person {
id: number;
name: string;
age: number;
birthOfDate: { year: number; month: number; day: number };
}
なお、以下のリンク先にあるように、TypeScriptは便利なMapped Typeをビルトインで提供しているので、先に目を通しておくと良いでしょう。
TypeScript-Handbook - Utility Types
また、後述するようなMapped Typeを自作せず、以下のようなライブラリを利用するのも有効です。
プロパティ名のUnion Typeを作る
interface が持つプロパティの名前を文字列のUnion Typeへと変換するには次のように定義します。
type Props = keyof Person;
この Props
は以下のように利用することができます。
// Props は以下と同じ型定義となる
// 'id' | 'name' | 'age' | 'birthOfDate'
const value: Props = 'name';
特定のプロパティの型を取り出す
interface から指定されたプロパティの型を取り出す Mapped Type は次のように定義します。
type PickType<T, K extends keyof T> = T[K];
この PickType
を利用すると、次のように指定したプロパティの型を取り出すことができます。
type Bod = PickType<Person, 'birthOfDate'>;
// Bod は以下と同じ型定義となる
// interface Bod {
// year: number;
// month: number;
// day: number;
// }
const birthOfDate: Bod = {
year: 2000,
month: 9,
day: 12
};
指定したプロパティだけを取り出した型を作る
interface から指定されたプロパティを持つ interface を定義するには、次のように Mapped Type を定義することができます。
type PickProps<T, K extends keyof T> = { [P in Extract<keyof T, K>]: T[P] };
// もしくは以下のように定義することも可能
// type PickProps<T, K extends keyof T> = { [P in K]: T[P] };
この PickProps
を利用すると、次のように任意のプロパティだけを持つ interface を新たに作り出すことができます。
type PersonBasic = PickProps<Person, 'age' | 'name'>;
// PersonBasic は以下と同じ型定義となる
// interface PersonBasic {
// name: string;
// age: number;
// }
const takeshi: PersonBasic = {
name: 'Takeshi',
age: 22
};
指定したプロパティを除外した型を作る
interface から指定されたプロパティを除外した interface を定義するには、次のように Mapped Type を定義することができます。
type RemoveProps<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P] };
この RemoveProps
を利用すると、次のように任意のプロパティを除外した interface を新たに作り出すことができます。
type PersonBasic = RemoveProps<Person, 'id' | 'birthOfDate'>;
// PersonBasic は以下と同じ型定義となる
// interface PersonBasic {
// name: string;
// age: number;
// }
const takeshi: PersonBasic = {
name: 'Takeshi',
age: 22
};
全てのプロパティを指定した型に変更する
interface で定義されている全てのプロパティの型を変更する Mapped Type は次のように定義することができます。
type PropsWithType<T, TYPE> = { [P in keyof T]: TYPE };
この PropsWithType
を利用すると、次のように指定した型と共に interface が作られます。
type PersonWithString = PropsWithType<Person, string>;
// PersonWithString は以下と同じ型定義となる
// interface PersonWithString {
// id: string;
// name: string;
// age: string;
// birthOfDate: string;
// }
const takeshi: PersonWithString = {
id: '123',
name: 'Takeshi',
age: '22',
birthOfDate: '2000-12-22'
};
共通のプロパティを取り出す
2 つの interface で定義されているプロパティのうち、共通のプロパティを取り出す Mapped Type は次のように定義することができます。
type Intersection<T, T2> = { [P in keyof T & keyof T2]: T[P] };
この Intersection
を利用すると、次のように共通のプロパティを持つ interface が作られます。
interface Pet {
name: string;
age: number;
weight: number;
}
type Animal = Intersection<Person, Pet>;
// Animal は以下と同じ型定義となる
// interface Animal {
// name: string;
// age: number;
// }
const takeshi: Animal = {
name: 'Takeshi',
age: 22
};
複数の interface をマージする
複数の interface をマージして全てのプロパティを統合するには &
を利用を利用することができます。次の例を見てみましょう。
interface Teacher {
schoolname: string;
}
type TeacherPerson = Teacher & Person;
// TeacherPerson は以下と同じ型定義となる
// interface TeacherPerson {
// id: number,
// name: string;
// age: number;
// birthOfDate: { year: number; month: number; day: number };
// schoolname: string;
// }
const takeshi: TeacherPerson = {
id: 123,
name: 'takeshi',
age: 33,
birthOfDate: {
year: 2000,
month: 9,
day: 12
},
schoolname: 'West school'
};
指定したプロパティを Union Type にする
interface の特定のプロパティを、他の型を受け取ることができるよう Union Type にするには、次のように Mapped Type を定義することができます。
type UnionProp<T, KEY extends keyof T, TYPE> = {
[P in keyof T]: (P extends KEY ? T[P] | TYPE : T[P])
};
これは次の例のように、id が数値にも文字列にもなり得るといったケースで便利です。
type PersonStringId = UnionProp<Person, 'id', string>;
// PersonStringId は以下と同じ型定義となる
// interface PersonStringId {
// id: number | string,
// name: string;
// age: number;
// birthOfDate: { year: number; month: number; day: number };
// }
const takeshi: PersonStringId = {
id: '1234',
name: 'takeshi',
age: 33,
birthOfDate: {
year: 2000,
month: 9,
day: 12
}
};
takeshi.id = 1234; // numberも格納できる
特定の型を持つプロパティだけを取り出す
interface から、特定の型を持つプロパティだけを取り出すには、次のように Mapped Type を定義することができます。
type PickPropsWithType<T, TYPES> = {
[P in { [K in keyof T]: T[K] extends TYPES ? K : never }[keyof T]]: T[P]
};
この PickPropsWithType
を利用すると、次のように指定した型で定義されたプロパティだけを取り出すことができます。
type PersonNumAndString = PickPropsWithType<Person, number | string>;
// PersonNumAndString は以下と同じ型定義となる
// interface PersonNumAndString {
// id: number,
// name: string,
// age: number;
// }
const takeshi: PersonNumAndString = {
id: 1234,
name: 'Takeshi',
age: 22
};
特定の型を別の型に再帰的に変更する
interface から、特定の型を別の型へと変更するために、次のような Mapped Type を定義することができます。
type DeepConvert<T, T1, T2> = {
[P in keyof T]: T[P] extends T1
? T2 :
T[P] extends Array<infer R>
? Array<DeepConvert<R, T1, T2>> :
T[P] extends object
? DeepConvert<T[P], T1, T2> :
T[P]
};
この DeepConvert
を使って、次の Person2
interface が持つ number のプロパティを string へと変更してみましょう。
interface Person2 {
id: number;
name: string;
available: boolean;
skills: Array<{id: number, name: string}>;
detail: {
age: number;
work: {
name: string;
startAt: number;
}
};
}
type Person2WithString= DeepConvert<Person2, number, string>;
// Person2WithString は以下と同じ型定義となる
// interface Person2WithString {
// id: string,
// name: string,
// available: boolean;
// skills: Array<{id: string, name: string}>;
// detail: {
// age: string;
// work: {
// name: string;
// startAt: string;
// }
// }
// }
const takeshi: Person2WithString = {
id: '123',
name: 'Takeshi',
available: true,
skills: [
{id: '12', name: 'C#'},
{id: '13', name: 'C++'},
],
detail: {
age: '33',
work: {
name: 'Engineer',
startAt: '22'
}
}
};
enum の値を Union Type に変換する
enum で定義された値の union type に変換は、TypeScript 4.1 で導入された Template Literal Types を利用することで実現できます。
enum Fruit {
Apple = 'apple',
Orange = 'orange',
Banana = 'banana'
}
type FruitName = `${Fruit}`
// FruitName は以下と同じ型定義となる
// 'apple' | 'orange' | 'banana'
const value: FruitName = 'apple'
スネークケースのキーをキャメルケースに変換する
APIのレスポンスとして受け取ったスネークケースのキー名をキャメルケースに変換したい場合、以下のようにMapped Typeを定義することができます。
type SnakeToPascal<T extends string> =
T extends `${infer Head}_${infer Tail}` ? `${Capitalize<Head>}${SnakeToPascal<Tail>}` : Capitalize<T>;
type SnankeToCamel<T extends string> = Uncapitalize<SnakeToPascal<T>>
type CamelKeyType<T> = T extends object ? { [K in keyof T as SnankeToCamel<Extract<K, string>>]: CamelKeyType<T[K]> } : T
上記の CamelKeyType
を利用することで、以下のようにスネークケース形式の型定義をキャメルケースへと変換することができます。
// スネークケースで定義されたinterface
interface SnakePerson {
birth_of_date: string,
first_name_and_last_name: string,
job: {
company_name: string,
started_from: number
}
}
// CamelKeyType を利用して、キャメルケースへと変換
type CamelPerson = CamelKeyType<SnakePerson>
// CamelPerson は以下と同じ型定義となる
// interface CamelPerson {
// birthOfDate: string,
// firstNameAndLastName: string,
// job: {
// campanyName: string,
// startedFrom: number,
// }
// }
let person: CamelPerson = {
birthOfDate: '2000/02/03',
firstNameAndLastName: 'Taro Hashimoto',
job: {
companyName: 'Google',
startedFrom: 2002
}
}