LoginSignup
62
55

More than 1 year has passed since last update.

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

Last updated at Posted at 2019-03-10

はじめに

TypeScript では、 Mapped TypeConditional 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
    }
}

参考

62
55
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
62
55