2020-08-10

TypeScript その3 ジェネリクス・アサーション

つづきをやっていく
今回はTS独特の概念を中心に書いていく

Type Compatibility

https://www.typescriptlang.org/docs/handbook/type-compatibility.html

TSでは変数の代入が行われる際、それぞれの型に互換性があるかどうかを常にチェックしている

let hoge: any
let fuga: string = 'fuga'

console.log(typeof hoge) // undefined
// エラーにならないので、any型とstring型には互換性があるということになる
hoge = fuga
console.log(typeof hoge) // string

let bar: string
let baz: number
let stringLiteral: "hoge" = "hoge" // stringのリテラル型

bar = baz // string型とnumber型には互換性がないのでエラー
bar = stringLiteral // stringのリテラル型はstringの一部なのでOK(当然numberのリテラルも同じ)

もう少し複雑な例をみる

// NamedとPersonには全く関係(依存性)がない

interface Named {
  name: string
}

class Person {
  constructor(public name: string) {}
}

let hoge: Named
hoge = new Person('hoge') // 問題なく代入できる

この例から、オブジェクトが代入できるかどうかの判定は、そのオブジェクトの型は関係がないことがわかる
(ちなみに typeof(new Person("aaa"))object である)

また、以下のように Named から name を除外してもエラーにならないが、
Named に別の age などを追加するとエラーになる

interface Named {
  // コメントアウトしても問題ない
  // name: string

  // これは hoge = new Person("hoge") の代入がエラーになる
  age: number
}

ここからわかることは「オブジェクトにおいて、代入される側にあるものは、代入する側にも存在しなければならない」ということ
メンバが存在するかどうかと、そのメンバの型が同じか互換性があるかどうかが大切
(クラスとinterfaceの継承関係なども無関係)

Generics

https://www.typescriptlang.org/docs/handbook/type-compatibility.html#generics

「1つ引数(string or number)を受け取り、受け取った型の値をreturnしたい」ような場合があり
内部の処理は同じだが、型の関係でDRYじゃない定義になってしまっている以下のようなコードがあったとする

const hoge  = (arg: string): string => arg
const hoge = (arg: number): number => arg

これを共通化してすっきりと書くことができるのがジェネリクスである
ジェネリクスを使ってリファクタすると以下のようになる

const hoge = <T>(arg: T): T => arg

T はジェネリクスではお決まりの抽象的な型の表現らしい
定義の段階では明示的に型を指定せず、呼び出された時に型が決定する関数と言った感じか
呼び出しは以下のように行う

hoge<number>(123)
hoge<string>("abc")

クラスでも使うことができる↓

class Hoge<T> {
  constructor(public fuga: T) {}

  bar(): T {
    return this.fuga
  }
}

console.log(new Hoge<number>(123).bar())
console.log(new Hoge<string>('abc').bar())
console.log(new Hoge<boolean>(true).bar())

Type Assertion

https://typescript-jp.gitbook.io/deep-dive/type-system/type-assertion

以下のように書くことを指す

let hoge: any = "hoge" // anyだと制限が緩い
let length = (hoge as string).length // stringであることは明確なので、型アサーションを行う
let length2 = (<string>hoge).length // これも同義

Const Assertion

https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html#const-assertions

TSでは const を使って代入したリテラル型は widening という挙動により、可変の変数へ代入するとリテラル型ではなくなるという仕様がある
どういうことかと言うと

const hoge = 'hoge'  // const hoge: "hoge"
const obj = { hoge } // const obj: { hoge: string }
obj.hoge = 'fuga'    // OK

のようなことができてしまうということ

const assertion を使うことで、この動作を抑制することができる

const hoge = 'hoge' as const // const hoge: "hoge"
const obj = { hoge }         // const obj: { hoge: "hoge" }
obj.hoge = 'fuga'            // NG

// const obj = { hoge } as const とした場合は
// const obj: { readonly hoge: "hoge" }
// と言う感じで readonly が付与される
// これはどれだけネストしていても付与してくれるので便利

参考: https://qiita.com/Takepepe/items/f39c249ed31e546ecb7c

Index Signature

例えばオブジェクトの初期化時にはどんなキーがあるのかわからないような場合
インデックスシグネチャを使うことで以下のように書くことができる

const hoge: { [index: string]: string } = {}
hoge.name = "aaa" // インデックスシグネチャにより name: stringはOK
hoge.age = 123    // age: numberは許容されていないのでNG

// もしnumberなど、他の型も許容したい場合は
// const hoge: { [index: string]: string | number }
// みたいに許可する型を追加する