ReactをTypeScriptで書く TypeScript編2

React関係なくなってきているのですがメモです。

追記: 2019/07/16
Omit型の項目を追記しました。

Tuple(タプル)
never型
unknown型
Index Signature(インデックスシグネチャ)
readonlyとReadonly型
Partial型とRequired型
Pick型とRecord型
Omit型
プロパティへのアクセス
keyof
inとMapped Types

TypeScript

Tuple(タプル)

Tuple(タプル)は複数の型を集めた構造体。

// Tupleにはstring、number、booleanが入る
type Tuple = [string, number, boolean];
const foo = ['bar', 123, true];

never型

never型は、発生しない値を表す型。

下記のconst errorは、関数の中でthrowして返らないのでneverになる。

const error = (message: string) => {
  throw new Error(message);
};

また、下記コードでは、if文の後のreturn barにはたどり着かないため、戻り値の型はneverになっている。

function foo(bar: string | number): boolean {
  if (typeof bar === 'string') {
    return true;
  } else if (typeof bar === 'number') {
    return false;
  }
  // barはnever型
  return bar;
}

型ガードの末に到達することがない箇所、switchやif文での網羅チェック、次回で触れるConditional typesでの除外などに度々出てくる。

Never Type - TypeScript Deep Dive 日本語版 より 網羅チェック(Exhaustive Checks)

unknown型

unknown型は、何が入るかわからない型。
どんな値が入るかわからないので、計算やプロパティの参照などができない。
下記は全て、それぞれ別の理由でエラーになる。

// undefinedが入る可能性があるのにlengthが使用されている
let foo: string | undefined;
console.log(foo.length);

// unknown型でlengthを使用している
let bar: unknown;
console.log(bar.length);

// 値が割り当てられる前にlengthが使用されている
let baz: string;
console.log(baz.length);

なんでもアリのany型と違って、type-safeな型として運用できる。

Index Signature(インデックスシグネチャ)

通常は、存在しないインデックス等にアクセスするとエラーになる。

const Foo = {
  bar: '文字列',
  baz: 123,
  3: 'keyが数値',
};

Foo.hoge = 0; // Foo型にhogeは存在しないためエラー
Foo['hoge'] = 0; // 暗黙的なanyとなる
Foo['3'] = 'hoge'; // 代入できる

index、またはkeyに型をつけるとアクセスできる。
keyをstringにすると、Foo.hogeのようなドット形式でもアクセスできる。

interface Dictionary {
  [key: string]: string | number;
  [index: number]: string;
}

const Foo: Dictionary = {
  bar: '文字列',
  baz: 20,
  3: 'keyが数値',
};

Foo['hoge'] = 'ふが'; // これもOK
Foo[1] = 'ふが'; // これもOK
Foo.hoge = 'ふが'; // これもOK
Foo['3'] = 'hoge'; // 代入できる

Foo[3] = 20; // これはエラー
Foo.hoge = false; // これもエラー
Foo.hoge.fuga = 'ふが'; // これもエラー

readonlyとReadonly型

readonlyをつけたプロパティは、変更不可能なプロパティになる。

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

const Taro: Person = {
  name: 'taro',
  age: 17,
};

Taro.name = 'jiro';
Taro.age = 18; // ageはreadonlyなのでエラー

全てのプロパティにreadonlyを付与した型を作りたい場合は、Readonly型がある。

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

type ReadonlyPerson = Readonly<Person>;

const Taro: ReadonlyPerson = {
  name: 'taro',
  age: 17,
};

Taro.name = 'jiro'; // nameもreadonlyになるのでエラー

Partial型とRequired型

Partial型は、型引数に指定したオブジェクトの型から、各プロパティの末尾に"?"をつけた型(全てのプロパティが必須でない型)を作成する。

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

// ageとlocationが入っていないのでエラー
const Taro: Person = {
  name: 'taro',
};

type PartialPerson = Partial<Person>;

// これはOK
const Jiro: PartialPerson = {
  name: 'jiro'
};

Required型はその逆で、全てのプロパティ名から"?"を消した型を作成する。

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

// これはOK
const Taro: Person = {
  name: 'taro',
};

type RequiredPerson = Required<Person>;

// ageとlocationが入っていないのでエラー
const Jiro: RequiredPerson = {
  name: 'jiro',
};

Pick型とRecord型

Pick型は、一部のプロパティを取り出して、新たな型を作成する。

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

// Person型からnameとlocationを取り出して作成
type NameType = Pick<Person, 'name' | 'location'>;

const Taro: NameType = {
  name: 'taro',
  age: 18, // ageは含まれないのでエラー
};

Record型は、union型をプロパティ名とした新しい型を作る。

type PersonRecordType = Record<'name' | 'age' | 'location', string>;

const Taro: PersonRecordType = {
  name: 'taro',
  age: 18, // ageはstringなのでエラー
  location: 'tokyo'
};

後述するkeyof Tと組み合わせて、既存のプロパティの型を一括変換することが可能。

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

// Personの各プロパティの型を、stringかnullだけにした型を作成
type PersonRecordType = Record<keyof Person, string | null>;

const Taro: PersonRecordType = {
  name: 'taro',
  age: 18, // ageはstring | nullなのでエラー
  location: 'tokyo',
};

Omit型

Omit型は、TypeScript3.5.1で採用された。
Pick型とExclude型(次回説明)を用いたもので、
オブジェクトの型から、指定したプロパティを省いた型を生成する。
(Omit型に入るプロパティ名はanyなので、存在しないプロパティを入れてもエラーになりません)

type Person = {
  name: string;
  age: number;
  gender: string;
}

const Taro: Person = {
  name: '太郎',
  age: 30,
  gender: 'male',
}

const Hanako: Omit<Person, 'age'> = {
  name: '花子',
  age: 30, // ageは省かれているのでエラー
  gender: 'female',
}

プロパティへのアクセス

こんな型がある場合

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

このようにして、Person内のプロパティが持つ型情報にアクセスできる。

let age: Person['age'] = '123'; // Person.ageはnumberなのでエラー

tupleの場合は、インデックスでアクセスできる。

type Person = [string, number, string];

let TaroAge: Person[1] = 18;

keyof

keyof 型で、その型のプロパティ名を集めたunion型として使用できる。

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

// 'name' | 'age' 型ができる
type label = keyof Person;

// 'nenrei'はlabel型にないのでエラーになる
const label_name: label = 'name';
const label_age: label = 'nenrei';

inとMapped Types

inは、その後に置かれたunion型を一つずつ取り出す。

type PersonData = 'name' | 'age' | 'location';

// PersonDataを{ プロパティ名: string }にした型を作成
const Taro: { [P in PersonData]: string } = {
  name: 'taro',
  age: 18, // ageはstringなのでエラー
  location: 'Tokyo',
};

このようにして、[P in K]: Tの構文を作ることができる。
さらに、型引数と、先ほど紹介したkeyof型、プロパティへのアクセスを使って、
[P in keyof T]: T[P]のようにすることができる。(Tは型引数)
これらをMapped Typesと呼ぶ。

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

type MappedPerson<T> = { [P in keyof T]: T[P] };

const Taro: MappedPerson<Person> = {
  name: 'taro',
  age: '18', // ageはnumberなのでエラー
  location: 'hoge'
};

上のコードだと…。

  1. const TaroのMappedPerson型は、Person型を型引数に取る
  2. keyof型を使い、Person型のプロパティ名でunion型を作り、それをinで回す
  3. それらのプロパティには、T[P]、つまりPerson['プロパティ名']の型をそのまま指定する

と言うわけで結局同じプロパティ名: 型を返しているだけど、少しいじることで、新しい型を作成することが可能になる。

例えば…。

// [P in keyof T]の後に?を付けてみる
type MappedPerson<T> = { [P in keyof T]? : T[P] };

とやることによって、各プロパティ名の末尾に"?"が付き、全て省略可能なプロパティのPerson型が作れる。

それってPartial型とやってること同じでは…と言うのはその通りで、
先ほど記載した組み込み型関数(Partial型、Required型、Readonly型、Pick型、Record型、Omit型)にはMapped Typesが使われている。

lib.es5.d.ts(docは省略)
type Partial<T> = {
    [P in keyof T]?: T[P];
};

type Required<T> = {
    [P in keyof T]-?: T[P];
};

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

Tには配列なども入れられる。
前はlengthなどのプロパティまで拾ってしまっていたけど、3.1で改善された。

const hoge = ['fuga', 'baz'];

type Stringify<T> = { [K in keyof T]: string };

// 配列内の型はstringなので18はエラー
const arr: Stringify<unknown[]> = ['taro', 18, 'tokyo'];

TypeScript 2.1のkeyofとかMapped typesがアツい - Qiita
TypeScript 3.1 の Mapped Tuple Typeについて - Qiita

まとめ

次回に続きます。