TypeScriptの既存の型から新たな型を定義する (Mapped Type)

# はじめに
TypeScript では、 [Mapped Type](https://www.typescriptlang.org/docs/handbook/advanced-types.html#mapped-types) や [Conditional Types](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#conditional-types)を利用することで、既存の interface から様々な型を定義することができます。

この記事では、以下の `Person` という interface を基にして、新たな型定義を行う例を紹介していきます。

```typescript
interface Person {
  id: number;
  name: string;
  age: number;
  birthOfDate: { year: number; month: number; day: number };
}
```

なお、以下のリンク先にあるように、TypeScriptは便利なMapped Typeをビルトインで提供しているので、先に目を通しておくと良いでしょう。
[TypeScript-Handbook - Utility Types](https://github.com/Microsoft/TypeScript-Handbook/blob/master/pages/Utility%20Types.md)

また、後述するようなMapped Typeを自作せず、以下のようなライブラリを利用するのも有効です。

- [type-fest](https://github.com/sindresorhus/type-fest)
- [utility-types](https://github.com/piotrwitek/utility-types)
- [SimplyTyped](https://github.com/andnp/SimplyTyped)
- [ts-toolbelt](https://github.com/millsp/ts-toolbelt)

# プロパティ名のUnion Typeを作る

interface が持つプロパティの名前を文字列のUnion Typeへと変換するには次のように定義します。

```typescript
type Props = keyof Person;
```

この `Props` は以下のように利用することができます。

```typescript
// Props は以下と同じ型定義となる
// 'id' | 'name' | 'age' | 'birthOfDate'

const value: Props = 'name';
```

# 特定のプロパティの型を取り出す

interface から指定されたプロパティの型を取り出す Mapped Type は次のように定義します。

```typescript
type PickType<T, K extends keyof T> = T[K];
```

この `PickType` を利用すると、次のように指定したプロパティの型を取り出すことができます。

```typescript
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 を定義することができます。

```typescript
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 を新たに作り出すことができます。

```typescript
type PersonBasic = PickProps<Person, 'age' | 'name'>;

// PersonBasic は以下と同じ型定義となる
// interface PersonBasic {
//   name: string;
//   age: number;
// }

const takeshi: PersonBasic = {
  name: 'Takeshi',
  age: 22
};
```

# 指定したプロパティを除外した型を作る

interface から指定されたプロパティを除外した interface を定義するには、次のように Mapped Type を定義することができます。

```typescript
type RemoveProps<T, K extends keyof T> = { [P in Exclude<keyof T, K>]: T[P] };
```

この `RemoveProps` を利用すると、次のように任意のプロパティを除外した interface を新たに作り出すことができます。

```typescript
type PersonBasic = RemoveProps<Person, 'id' | 'birthOfDate'>;

// PersonBasic は以下と同じ型定義となる
// interface PersonBasic {
//   name: string;
//   age: number;
// }

const takeshi: PersonBasic = {
  name: 'Takeshi',
  age: 22
};
```

# 全てのプロパティを指定した型に変更する

interface で定義されている全てのプロパティの型を変更する Mapped Type は次のように定義することができます。

```typescript
type PropsWithType<T, TYPE> = { [P in keyof T]: TYPE };
```

この `PropsWithType` を利用すると、次のように指定した型と共に interface が作られます。

```typescript
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 は次のように定義することができます。

```typescript
type Intersection<T, T2> = { [P in keyof T & keyof T2]: T[P] };
```

この `Intersection` を利用すると、次のように共通のプロパティを持つ interface が作られます。

```typescript
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 をマージして全てのプロパティを統合するには `&` を利用を利用することができます。次の例を見てみましょう。

```typescript
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 を定義することができます。

```typescript
type UnionProp<T, KEY extends keyof T, TYPE> = {
  [P in keyof T]: (P extends KEY ? T[P] | TYPE : T[P])
};
```

これは次の例のように、id が数値にも文字列にもなり得るといったケースで便利です。

```typescript
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 を定義することができます。

```typescript
type PickPropsWithType<T, TYPES> = {
  [P in { [K in keyof T]: T[K] extends TYPES ? K : never }[keyof T]]: T[P]
};
```

この `PickPropsWithType` を利用すると、次のように指定した型で定義されたプロパティだけを取り出すことができます。

```typescript
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 を定義することができます。

```typescript
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 へと変更してみましょう。

```typescript
interface Person2 {
  id: number;
  name: string;
  available: boolean;
  skills: Array<{id: number, name: string}>;
  detail: {
    age: number;
    work: {
      name: string;
      startAt: number;
    }
  };
}
```

```typescript
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](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/#template-literal-types) を利用することで実現できます。

```typescript
enum Fruit {
    Apple = 'apple',
    Orange = 'orange',
    Banana = 'banana'
}

type FruitName = `${Fruit}` 

// FruitName は以下と同じ型定義となる
// 'apple' | 'orange' | 'banana'

const value: FruitName = 'apple'
```

# スネークケースのキーをキャメルケースに変換する

APIのレスポンスとして受け取ったスネークケースのキー名をキャメルケースに変換したい場合、以下のようにMapped Typeを定義することができます。

```typescript
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` を利用することで、以下のようにスネークケース形式の型定義をキャメルケースへと変換することができます。

```typescript
// スネークケースで定義された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
    }
}
```

# 参考

- [Advanced Types -TypeScript](https://www.typescriptlang.org/docs/handbook/advanced-types.html)
- [TypeScript 2.8](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html)
- [TypeScript: Create a condition-based subset types](https://medium.com/dailyjs/typescript-create-a-condition-based-subset-types-9d902cea5b8c)
- [TypeScript Handbook の Advanced Types をちょっとだけ掘り下げる - その 7 Mapped types](https://qiita.com/kobanyan/items/2fdd6b0ed7411ff9980e)
- [TypeScript: remove key from type/subtraction type](https://stackoverflow.com/questions/41476063/typescript-remove-key-from-type-subtraction-type)