ReactをTypeScriptで書く TypeScript編3

前回からの続きです。
React関係ないのですがメモです。

TypeScript

Exclude型とExtract型
NonNullable型
Parameters型とConstructorParameters型
ReturnTypeとInstanceType
Conditional Typesとinfer
ThisType

Exclude型とExtract型

それぞれ組み込み型関数の一つで、<T, U>二つの型引数をとる。

Exclude型は、Uの型を除外した型を作成できる。

type AgeType = number | string;

let yourAge: AgeType = 21;

let myAge: Exclude<AgeType, number> = 30; // numberは除外されているのでエラー
myAge = '永遠の17歳'; // これはOK

Extract型は、Uの型に当てはまる型を作成できる。

type genderType = 'male' | 'female' | undefined;

// genderTypeからundefinedだけを取り出す
let aozoraGender: Extract<genderType, undefined>;

aozoraGender = 'male' // これはエラー
aozoraGender = 'female'; // これもエラー

NonNullable型

NonNullable型は、nullやundefinedを除外した型を作成する。

// elemはHTMLElement型かundefined型
type elem = HTMLElement | undefined;

// NonNulableElemはHTMLElement型
type NonNulableElem = NonNullable<elem>;

Parameters型とConstructorParameters型

Parameters型は、引数に指定された型をタプルにして返す。

type getPersonType = (name: string, age: number) => void;

// [string, number, string]のtupleが生成される
type PersonParametersType = Parameters<getPersonType>;

// [1]はnumberなのでエラー
const params: PersonParametersType = ['taro', '17'];

let age: PersonParametersType[1] = 18; // これはOK

可変長引数にも対応している。

type getPersonType = (name: string, ...args: string[]) => void;

// [string, number, string[]]のtupleが生成される
type PersonParametersType = Parameters<getPersonType>;

let age: PersonParametersType[999999999] = '18'; // これはOK

ConstructorParametersは、Parameters型のconstructor版。
型引数には<typeof class名>が使用できる。

type getPersonType = (name: string, age: number, ...args: string[]) => void;

class Person {
  name: string;
  age: number;
  location: string;
  constructor(name: string, age: number, location: string) {
    this.name = name;
    this.age = age;
    this.location = location;
  }
  /* いろいろ */
}

// [string, number, string]が入る
type PersonConstructorTypes = ConstructorParameters<typeof Person>;

ReturnTypeとInstanceType

ReturnTypeは、関数の戻り値の型を取得する。
複数の型が返る場合は、union型で表現される。

type getHogeType = (hoge: string) => string;
type getFugaType = (fuga: string | number) => string | number;

type Hoge = ReturnType<getHogeType>; // string
type Fuga = ReturnType<getFugaType>; // string | number

const Result = (label: string) => {
  switch (label) {
    case 'あたり':
      return 'あたり';
    case 'はずれ':
      return 820;
    case 'あたま':
      return false;
    default:
      return;
  }
};

// false | "あたり" | 820 | undefined
type ResultType = ReturnType<typeof Result>;

InstanceTypeは、インスタンスの型を返す。

class Person {
  public name?: string;
  public age?: number;
  public location?: string;
}

const Taro = new Person();
Taro.age = 18; // PersonのageはnumberなのでOK

const Jiro: InstanceType<{ new (): Person }> = {
  name: 'jiro',
  age: '18', // Personのageはnumberなのでエラー
  location: 'tokyo',
};

Conditional Typesとinfer

三項演算子のように表現して、当てはめる型を評価させることができる。

type Foo<T, U, X, Y> = T extends U ? X : Y;

TがUの型ならX型を、そうでなければY型を返すのが基本形。
仕組みはこちらが参考になる。
TypeScript 2.8 の Conditional Types について - Qiita
TypeScript 2.8.1 変更点 - Qiita

例として、「Uに指定した型を、Tから取り除く」Diff型の挙動を確認してみる。

lib.es5.d.ts
// TがUならnever, それ以外はTの型を返す
type Diff<T, U> = T extends U ? never : T;

// 'taro' | 18 | 'tokyo'の中で、stringのものを除外する
type Bar = Diff<'taro' | 18 | 'tokyo', string>;

Tには'taro' | 18 | 'tokyo'のunion型が入っている。
これについてTypeScriptは、下記のように1つずつ取り出してT extends U ? X : Y;の形で推測する。

// (※イメージです)
type DiffNoNakami =
  ('taro' extends string ? never : 'taro') |
  (18 extends string ? never : 18) |
  ('tokyo' extends string ? never : 'tokyo');

'taro'と'tokyo'はstringなので、never。
18はstringではないので、18がそのまま返ってくる。
結果はこのようなunion型になる。

type DiffNoAto = never | 18 | never;

never型は、全ての型の部分型(subtype)で、union型からは省略されるようになっている。
そのため、最終的には下記のように'18'だけが通る型になる。

type DiffNoAto = never | 18 | never; // neverはunion型から取り除かれる

let res: DiffNoAto = 18; // DiffNoAto型は18のみ

このDiffという型、Exclude型とやってること同じでは…と言うのはその通りで、
先ほどまでに紹介した組み込み型関数(Exclude型、Extract型、NonNullable)にはConditional Typesが使われている。

lib.es5.d.ts
type Exclude<T, U> = T extends U ? never : T;

type Extract<T, U> = T extends U ? T : never;

type NonNullable<T> = T extends null | undefined ? never : T;

また、Conditional Typesではinferという特殊なワードも使える。
extends句の中で使え、条件が真の時、その値や戻り値の型を推測して型変数に入れることができる。

interface Person {
  name: string;
  age: number;
  gender: string;
}

interface Product {
  name: string;
  price: number;
}

type PickAgeType<T> = T extends { age: infer U } ? U : never; // Tにageプロパティがあれば、その型を返す

let TaroAge: PickAgeType<Person> = '18'; // PickAgeType<Person>はnumberなのでエラー

let HogePrice: PickAgeType<Product>; // PickAgeType<Product>はnever

上記だと、Tにageプロパティがあれば、その型をUがキャプチャする。
そのUは真式(真の時の式)で使用できるので、ageプロパティの型を返せる。
Personはnumber型のageプロパティを持っているので、number型が返る。

このinferを使って、関数の戻り値や引数の型を当てはめる、という芸当が可能になる。
ParametersやConstructorParameters型、ReturnType、InstanceTypeにはinferが使われている。

lib.es5.d.ts
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;

type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;

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

type InstanceType<T extends new (...args: any[]) => any> = T extends new (...args: any[]) => infer R ? R : any;

再帰的に定義することはできない。

その他参考
TypeScript 2.8 · TypeScript
TypeScript 2.8: Conditional Types — Marius Schulz
TypeScript2.8 Conditional Types 活用事例 - Qiita

ThisType

ThisTypeは、空のinterfaceを返す。

interface ThisType<T> { }

この型を使うと、thisにTで指定した型を当てはめることができる。
最低でも、noImplicitThis(thisの暗黙的なanyを許可しない)の設定が必要。

interface Person {
  name: string;
  age: number;
}

// thisの型がPersonになる
const TaroMethodList: ThisType<Person> = {
  taroName(): void {
    this.name = 'taro';
  },
  taroAge(): void {
    this.age = '18'; // this.ageはnumberなのでエラー
  },
};

まとめ

もうちょっと知見を得られたら続きます。