型で学ぶTypeScript 01
2021-03-15

型で学ぶTypeScript

インターセクション型

A 且つ Bのように表すことのできるもの。日本語では交差型とも。

type Product = {
  name: string
}

type Engineer {
  name: string
  role: string
}

type ProductOwner {
  name: string
  products: Product[]
}

例えば、このようにエンジニアとプロダクトオーナーという2つの役職があるとして、この2つの役職を兼ねる人材がいるとする。 そういった人物がいた場合、

type EngineerProductOwner = Engineer & ProductOwner

このようにして、2つの型を兼ねた型を生み出すことができる。この時、EngineerProductOwnerは、それぞれが持つプロパティを全て持っていないといけない。

const productA = {
  name: 'projectA',
}

const productB = {
  name: 'projectB',
}

const mofmofEngineer: EngineerProductOwner = {
  name: 'saboten',
  role: 'fullstack',
  products: [productA, productB],
}

ちなみにtypeではなくinterfaceで行う場合は

interface Product {
  name: string;
}
interface Engineer {
  name: string;
  role: string;
}
interface ProductOwner {
  name: string;
  products: Product[];
}
interface EngineerProductOwner extends Engineer, ProductOwner { }

のようになります。 interfaceは複数をextendできる。最後にからのオブジェクトを指定するのを忘れないように。

ちなみに、あまり出ないとは思うが、numberかstring, numberかbooleanの型の組み合わせは numberになる。A且Bで可能な範囲を推論する。

typeかinterfaceどっち使うのか?問題。

個人的にはtypeを使っとけばいいんじゃないか?派です。 interfaceとtypeの違いについては、少しググれば色々出てくると思いますが、改めて整理がてらメモしておきます。

interfaceは型の宣言 == 型に名前をつけることができる。typeは無名の型オブジェクトにエイリアスを与える(代入している)と見ることができる。

type Product = {
  name: string
}
interface Product {
  name: string
}

typeにある=文を見るとわかると思う。

また、上記で挙げたようなextendsの方法以外にも拡張する方法がある

interface Product {
  name: string
}
interface Product {
  budget: number
}

const productA: Product = {
  name: 'projectA',
}

このような場合、Productの型は下記のようになっているので、productAはエラーになる。

interface Product {
  name: string
  budget: number
}

同名で宣言した場合、新しいプロパティの定義追加になる。よって、誰かが予期しない場所で追加していたり、誤って同名で定義するとエラーになる。 こういった事故を防ぐためにも、typeを使えばいいんじゃないか?派である。

型情報を使って絞り込みを行いたい

型を使っているので、使っている型によって処理を分けたり、型の恩恵を受けるために型を絞り込んで使いたいときが出てくる。TypeGured。

  1. typeofを使う シンプルなパターン。if文によりparamaterの型を限定して、その上で処理を行う。
const upperCaseOrDouble = (x: string | number) => {
//処理
}

この中で、もしxが文字列型だった場合には大文字に変換したいときがある。 こういった場合に、typeofを使うと下記のようにかける。

const upperCaseOrDouble = (x: string | number) => {
  if(typeof x === 'string'){
    x.toUpperCase()
  }
}

if文で既にstring型であると絞り込まれているので、型ファイルに定義されているstring型用のメソッドが予測で出てくる様になっている。

  1. in を使う方法。 先程のtypeof演算子での比較は全てのパターンに対処できるわけではない。
type Engineer = {
  name: string
  role: string
}

type ProductOwner = {
  name: string
  products: Product[]
}

type BackOffice = {
  name: string
  hasKey: boolean
  telNum: number
}
type mofmofStaff = Engineer | ProductOwner | BackOffice

const describePosition = (staff: mofmofStaff) => {

}

次のような例を出して考えてみる。mofmofスタッフは、エンジニアであるか、プロダクトオーナーであるか、バックオフィスのどれかの役職についていると考えます。

describePosition関数は、役職によって返り値を何かしらに変えたい関数だとしましょう。 この時、typeofではstaffオブジェクトがどのタイプのオブジェクトなのかということはわからない。

typeofが返すことのできる形は、

の8種類であり、今回で言えばstaffはobjectに該当する。一方で、objectではあっても、それがbackOfficeなのかengineerなのか、といったことまではわからないのである。 このときに使用できる選択肢の一つとして挙げられるのが、in演算子である。 in演算子を使った絞り込みの例として下記のようなもの をあげてみる

const describePosition = (staff: mofmofStaff) => {
  if ('telNum' in staff) {
    return staff.telNum
  }
}

このように、'telNumが呼べるもの'だけ分岐処理を書くことができる。今回で行くと、telNumが呼べるのはバックオフィスのスタッフだけなので、このif分岐の中のstaffの型はBackOfficeだとエディタが推論してくれる。 一方で、nameなどの、今回のどのタイプでも持っているようなキーをinに指定してしまうと、絞り込みができないので、

のようにエラーが出る。

3.insetanceofを使う インスタンスがいずれかのクラスを元にして生成さている場合は、instanceofが使える。 適当な例を上げるとこんな感じ。

class Dog {
  speak() {
    console.log('bow')
  }
}

class Cat {
  speak() {
    console.log('nya')
  }
}

type Animal = Dog | Cat

const instanceCall = (pet: Animal) => {
  if (pet instanceof Dog) {
    //犬のときの処理
  }
}

バックエンドをRailsでかくことが多いので、ts側でクラスの定義を書くことはあまりないが、覚えておくと役に立つときがいつか来るかもしれない。 ちなみに、ダックタイピング的な感じで、処理内容に該当するメソッドを定義したオブジェクトをわたしても呼ばれない。insetanceofは、指定された型からできたインスタンスであるかどうかを見ているから。

タグ付きユニオン

型というよりはデザインパターン?な気がする。

class Dog {
  kind: 'dog' = 'dog'
  speak() {
    console.log('bow')
  }
}

class Cat {
  kind: 'cat' = 'cat'
  speak() {
    console.log('nya')
  }
}

上記のようにkindという名前で(kindの部分は何でもいい),タグをつける。 これによって、下記のような分岐が可能になる。

const instanceCall = (pet: Animal) => {
  switch (pet.kind) {
    case 'cat':
    //猫のときの処理
    case 'dog':
    //犬のときの処理
  }
}

複数パターンが有るときは、instanceofを使うよりも、こちらで分岐をしたほうがスマートに見える時もある。

型アサーション

typescriptが推論/分析した型を手動で上書きする事。 https://typescript-jp.gitbook.io/deep-dive/type-system/type-assertion

例えば、idによってEmailアドレスのインプットフォームを指定するとする。(inputEmailは構成上、inputタグであると仮定する)

const inputEmail = document.getElementById('email')

このとき、inputEmail変数は、

const inputEmail: HTMLElement | null

このように型推論される。この時、inputEmailにはvalueというinputに入力された値が取れるはずで、それを指定して処理を行いたい。

こんなときに取れる手段。 1.

const inputEmail = <HTMLInputElement>document.getElementById('email')

先頭に型を宣言してつける方法。

2.

const inputEmail = document.getElementById('email') as HTMLInputElement

後半にasをつける。

どっちでもいいが、reactを使っている時(jsx/tsx)を使うときは、タグと見間違えたりするのでasを使うほうが好き。

Non-null-assertion

「nullじゃないぞ、絶対にだ」

const inputEmail = document.getElementById('email')!

このように書くことで、推論からnullが消える。あんまり使いたくないやつではあるが、使わなければ行けないときもある。

indexシグネチャー

使い時がわからないので教えて欲しいやつその1。

プロパティへの添字アクセス(キーアクセスに対しての方情報を定義できる。

type Designer = {
  name: string
  [key: string]: string
}

このように書くと、string型をキーにして、stringを保持できる。

type Designer = {
  name: string
  [key: string]: string
}

const designer:Designer = {
  name: 'designer',
  age: '20',
  address: 'tokyo'
}

本来であれば、nameだけが明示されているデザイナー型だが、ageやaddressなどを指定することができる。ちなみに、インデックスシグネチャを定義した場合、インデックスシグネチャで指定した値の型以外で他の値を明示することはできない。

柔軟に入ってくる値が変わってくるObjectを使う場合に 使用する機会が訪れる?のか?

関数のオーバーロード

const upperCaseOrDouble = (x: string | number) => {
  if (typeof x === 'string') {
    return x.toUpperCase()
  }else {
    return x
  }
}

上記のような関数を実行した結果を変数に入れて再利用し用とした場合、TypeScriptの推論はいい感じにしてくれない事がある。

const result = upperCaseOrDouble(22)

このときのresultの型推論はnumber出会って欲しいが、残念なことにeditorはstring or numberの結果を返す。 関数のオーバーロードというものを使用して、戻り値を制限することができる。

type UpperCaseOrDouble = {
  (x: string): string
  (x: number): number
}
const upperCaseOrDouble: UpperCaseOrDouble = (x: string | number): any => {
  if (typeof x === 'string') {
    return x.toUpperCase()
  } else {
    return x
  }
}

const result = upperCaseOrDouble(22)

anyの部分は string | numberとしていたらエラーになったので、anyにした。

OptionalChaning

GraphQLCodeGenを使う時はめっちゃお世話になる。

関連データがチェインで取得できない場合に、エラーが起きないようになる。具体的にはない場合はundefinedが返ってくる。

data.user?.name

のように使う。

NullishCoalescing

??

である。

const user = data.user ?? 'no-user'

左辺がundefined or nullでないときだけ、userに値が入る。三項演算子などでも同じようなことができるが、 0であったり、''(空文字)を許容したい場合はこっちを使おう。

LookUp型

型の持っているメンバーの型にアクセスしたい時があるとする。

type Designer = {
  id: number
  name: string
  works?: {
    title: string
    description: string
    date: Date
  }
}

例えば、デザイナータイプがある場合のidの型を知りたい時。

type id = Designer["id"]

と書くことで指定することができる。

レストパラメータに配列とか使いたい時

レスとパラメータとは不特定多数の引数を配列として受け取る構文

const numberArrayDouble = (...numbers: number[]) => {
  return numbers.map((number) => {
    return number * 2
  })
}

console.log(numberArrayDouble(1, 2, 3, 5))

機能制限を行ったタプル型も利用できる。

const numberArrayDouble = (...array: [number, string, boolean, ...number[]]) => {
  return array.map(obj => {
    console.log(obj)
  })
}

console.log(numberArrayDouble(1, 'string', true, 1, 3, 3))

const Assation

as const を用いて、変化不可能な形にする。

const array = [10, 20] as const
//=> const array: readonly [10, 20]

このように、as const をつけることによって、readonly修飾子が付き、また、中身も具体的なリテラル値になる。

型でtypeofする

const suboten = {
  name: 'saboten',
  role: 'fullstack'
}

例えば、上記のようなオブジェクトがあったとする。このオブジェクトから型情報を生成したい時にこのように使う。

type mofmofType = typeof suboten

このようにすることによって、mofmofTypeの中身は、

type mofmofType = {
    name: string;
    role: string;
}

このようになる。地味に便利。