PlayGroundで覚えるTypeScriptの高度な型を使った便利な記法

**身内の勉強会用の資料です。**


PlayGroundでコードを動かすことで、なんとなく何が起こるかを知るための資料です。
まともな説明はしないので、ちゃんと知りたい人は公式サイトとか読みましょう。(今回紹介したコードもだいたい[公式サイト](https://www.typescriptlang.org/docs/handbook/advanced-types.html)に載ってます)

本文の一番下に動作確認用のコピペコードがあるので、[PlayGround](https://www.typescriptlang.org/play/index.html)に貼り付けて動作を確認してみましょう。

また、ここで紹介する以外にもTypeScriptの組み込み関数には以下のようなものがあるので興味があれば見てみてください

* [TypeScript特有の組み込み型関数](https://log.pocka.io/posts/typescript-builtin-type-functions/)

## 型のおさらい

以下のようなUserオブジェクトを使って例示する

```.ts

const user = {
  name: 'testuser',
  id: 1,
  obj: {
    obj2: {
      aaa: 'aaaa'
    },
    obj3:{
      ccc: 'cccc'
    },
    bbb: 'bbbb',
  }
}
```

#### typeof

オブジェクトの型を返す。以下のような感じ

```.ts
type User = typeof user
```

<img width="248" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/ffd00f71-0721-d9ee-5cc1-ec1892e3916a.png">


#### keyof

オブジェクト型のキーをUnion Typeに変換する。以下のような感じ

```.ts
type A = keyof User
```

<img width="293" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/d9ab5394-252c-5643-0441-c2276a30cd19.png">


#### T[K]

オブジェクト型のプロパティの型を返す。以下のような感じ

```.ts
type B = User['name']
```

<img width="293" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/552ab00f-1e95-aef4-e7f8-0c974452830e.png">


#### in

{[K in (Union Type)]: P} はUnion TypeをMapに展開する。以下のような感じ

```.ts
type C = {[K in keyof User]: string}
type D = {[K in keyof User]: K}
type E = {[K in keyof User]: User[K]}   // = User型と同じ
type F = {[K in keyof User]: User}
```

<img width="270" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/b6be870b-e124-6d88-2bbf-4278caf822e2.png">

<img width="270" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/eb713689-b781-8499-2b14-3e0c26d85dc1.png">

<img width="423" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/80bb069e-3b06-d659-a1bc-253fa1893e4c.png">

<img width="423" alt="image.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/93999/bdb4f8f2-ec26-d664-9b24-5762b553c9de.png">


## オブジェクトのキーを推論する

上で紹介した `keyof` などは以下のような時に使える

```.ts
/**
 * 引数に指定したオブジェクトのキーの値を所得する
 */
function propGet<T, K extends keyof T>(obj: T, key: K): T[K]{
    return obj[key]
}
```

このような関数の型を上のようにつけておくと、以下のように、第一引数の型を使って賢く推論してくれる

``` .ts
propGet(user,'name') // => 返り値の型がstringになる
propGet(user,'id')   // => 返り値の型がnumberになる
propGet(user,'nana') // => nanaというプロパティは存在しないのでコンパイルエラー
```

### classの中で使う

以下のようなgetProp関数をもつ`DocumentWrapper`classを作っておく

```.ts
// getPropを定義したDocumentWrapper
abstract class DocumentWrapper {
  /**
   * 引数に指定したキーの値を所得する
   */
  getProp<T extends keyof this>(key: T): this[T]{
    return this[key]
  }
}
```

ちなみにここでは`this`型は自分自身の型を返してくれる

このDocumentWrapperを継承したclassを作る

```.ts
class Person extends DocumentWrapper {
  id: number | null = null
  name = ''
  obj = {
    aaa: 'aaa',
    bbb: 123
  }
}
```

以下のように使える

```.ts
const person = new Person()

person.getProp('id') // => 返り値はnumber | null型
person.getProp('name') // => 返り値はstring型
person.getProp('nana') // => nanaというプロパティはPersonに存在しないのでコンパイルエラー
```

## 定数の推論を行う

keyとvalueが同じ値をもつ連想配列を生成する関数を以下のように作り、定数として利用できるようにする

``` .ts
/**
 * 引数にいれた文字列の配列をkeyとvalueにもつオブジェクトを返す
 */
function generateEnum<T extends string>(strArr :T[]): {[K in T]: K } {
    return strArr.reduce((acc, cur) => {
    acc[cur] = cur;
    return acc;
  }, Object.create(null));
}
```

以下のように使うことができる

```.ts
const POSITION = generateEnum(['top','center','bottom'])
```

これは以下と同じ、

```.ts
const POSITION = {
    top: 'top',
    center: 'center',
    bottom: 'bottom'
}
```

この定数のみをUnion Typeとして引数や返り値に指定したい場合は、 `keyof typeof` を使うことができる。
以下のような感じ

``` .ts
function reversePosition(position: keyof typeof POSITION): keyof typeof POSITION{
  switch(position){
    case 'top': {
      return 'bottom'
    }
    case POSITION.center: {
      return 'center'
    }
    case 'bottom': {
      return 'top'
    }
  }
}
```

``` .ts
reversePosition('top') // => topという文字列は定数の中に存在するのでok
reversePosition(POSITION.top) // => 当然POSITION.topを指定してもok
reversePosition('button') // => buttonという定数は存在しないのでコンパイルエラー
```

## コピペ用

動作確認用のコピペコード

```.ts

const user = {
  name: 'testuser',
  id: 1,
  obj: {
    obj2: {
      aaa: 'aaaa'
    },
    obj3:{
      ccc: 'cccc'
    },
    bbb: 'bbbb',
  }
}

// typeof はオブジェクトの型を返す
type User = typeof user

// keyof は型のメンバをUnion Typeで返す
type A = keyof User

// T[K]はプロパティアクセス型になる
type B = User['name']

// {[K in (Union Type)]: P} はUnion TypeをMapに展開する
type C = {[K in keyof User]: string}
type D = {[K in keyof User]: K}
type E = {[K in keyof User]: User[K]}   // = User型と同じ
type F = {[K in keyof User]: User}




// Prop.get()みたいなやつを扱う

/**
 * 引数に指定したオブジェクトのキーの値を所得する
 */
function propGet<T, K extends keyof T>(obj: T, key: K): T[K]{
    return obj[key]
}

// 第2引数を 'name' | 'id' | 'obj' のようにきちんと推論してくれる
// 返り値のidのほうも number型としてきちんと推論してくれる
const id = propGet(user,'id')

const age = propGet(user,'age') // 'age' は存在しないのでエラー

if (propGet(user,'name') === 9){} // nameプロパティの型はstringなので、この条件式は常にfalseになるため、エラーになる


// classの中でProp.getを扱う

// getPropを定義したDocumentWrapper
abstract class DocumentWrapper {
  /**
   * 引数に指定したキーの値を所得する
   */
  getProp<T extends keyof this>(key: T): this[T]{
    return this[key]
  }
}

class Person extends DocumentWrapper {
  id: number | null = null
  name = ''
  obj = {
    aaa: 'aaa',
    bbb: 123
  }
}

const person = new Person()

// 第引数を 'name' | 'id' | 'obj' | getProp のようにきちんと推論してくれる
// 返り値のidのほうも number | null型としてきちんと推論してくれる
const personId = person.getProp('id')



// 定数を扱う

/**
 * 引数にいれた文字列の配列をkeyとvalueにもつオブジェクトを返す
 */
function generateEnum<T extends string>(strArr :T[]): {[K in T]: K } {
    return strArr.reduce((acc, cur) => {
    acc[cur] = cur;
    return acc;
  }, Object.create(null));
}


const POSITION = generateEnum(['top','center','bottom'])

function reversePosition(position: keyof typeof POSITION): keyof typeof POSITION{
  switch(position){
    case 'top': {
      return 'bottom'
    }
    case POSITION.center: {
      return 'center'
    }
    case 'bottom': {
      return 'top'
    }
  }
}

reversePosition('top') // => topという文字列は定数の中に存在するのでok
reversePosition(POSITION.top) // => 当然POSITION.topを指定してもok
reversePosition('button') // => buttonという定数は存在しないのでエラー

```