【技術書記録005-1 型の基本】プログラミングTypeScript スケールするJavaScriptアプリケーション開発

今回の技術書

転職先でTypeScriptを使い始めたので、会社の書籍購入制度を活用して支給してもらった一冊。

重厚な一冊だった。部分的に複雑で難しい内容のものや、実務等で携わっていないと実感が湧かないものもあったので、飛ばし飛ばしになったが一通り読み終えた。
一回読んだだけで簡単に身に付く内容ではないので、今後もリファレンス的に活用していこうと思う。

本書については全体を一から順にまとめるのではなく、発展的で難しいなと感じた部分を中心に、自分の理解を促すために要所を抜き出したい。
ボリューム満点な一冊なので、記事も何本かに分けることにした。何本になるか分からない分量だけど…。
今回は型の基本について。その前にTypeScriptを使ってみた感想を書いておく。
※使用期間はまだまだ短い人の感想。


TypeScriptに対する自分の所見

使い始める前は、TypeScriptに対して敷居の高さを感じていた。生JSですら怪しい自分が、果たして使えるものなのかと。
実際のところ「型」の活用に関して、複雑な書き方を理解するのは難しい。 しかし、使い始めて2ヶ月の私ですらTypeScript + コードエディタの補完に慣れると、もう戻れないなと感じている。
もちろんanyじゃなくて何についても型を書くことが前提になるが、事前に引数や変数・戻り値の型をチェックしてエラーを吐いてくれるので、極力実行時エラーを減らすことができる。
個人開発をしていたときに悩まされた「NaNって何だ…どこでエラってるんだ…」といった現象を減らせるし、エラーの原因を特定しやすくなる。
そもそも常にデータ型を気にしてコードを書いていくので、エラーを起こす頻度も減っていると感じる。
これからフロント開発するならTypeScriptを積極的に採用すべきだと思う。記述量の増加とエラーの減少はトレードオフどころか、お釣りが来るんじゃないだろうか。ありがたみは今後さらに実感していくことになるだろう。


型について

f:id:itiiki:20211230164356p:plain
型の階層構造

本書から図を拝借。まずは型の基本は抑えておかないと。この中からいくつか取り上げる。
今のところ、bigintとsymbolは使ってないな。symbolは今ひとつ使い所が分かっていない。
全部は詳しく取り上げないので、詳しくは本書を読むべし(言い訳)。

any

使用したり発生したりするとコンパイラに怒られる。「何でも」を意味するので、anyを書くとTypeScriptを使う意味がなくなる。極力使用は避けよう。

unknown

本当に型が分からない値がある場合、anyではなくunknownを推奨。if文等でunknownの型チェック・絞り込みを行なった上で、その値を使用する。

boolean

boolean自体は自明なので省略。ここで出てくるリテラルが特徴的。

const c = true        // booleanではなく型'true'になる
let e: true = true    // true
let f: true = false   // 型'false'を型'true'に割り当てることはできません。

リテラルなので、boolean以外も当然可能。

let a: 'ほげ' = 'ふが'     // 型 '"ふが"' を型 '"ほげ"' に割り当てることはできません。
let eleven: 11 = 22      // 型 '22' を型 '11' に割り当てることはできません。
オブジェクト

TypeScriptにおいて、オブジェクトは構造的型付けされる。Java等の名前的型付けと異なる。これはコードを見る方が早い。

type Ramen = {
  soup: string,
  soupAmount: number,
}
type Udon = {
  soup: string,
  soupAmount: number,
}

let muteppo: Ramen = {
  soup: 'とんこつ',
  soupAmount: 400
}
let makino: Udon = {
  soup: 'だし醤油',
  soupAmount: 300
}

muteppo = makino
console.log(muteppo)     // { soup: 'だし醤油', soupAmount: 300 }

名前的型付けではmuteppo = makinoでエラーが出るが、TypeScriptでは代入できる。
これはオブジェクトの形状で型を推論しているから。
ここでtypeとしているのはエイリアス。「このオブジェクト(あるいはstring等)は、この名前の型です」と宣言している。エイリアスは同じスコープ内で重複して宣言できない。これはinterfaceと異なるところ。interfaceについては後日の記事にて。

他には、以下のプロパティがオブジェクトに設定できる。

let KaisenDon: {
  readonly rice: string, ・・・(1)
  fish: string[], ・・・(2)
  wasabi?: true, ・・・(3)
  [key: number]: boolean ・・・(4)
}

(1) readonly修子:フィールドを読み取り専用に指定。初期値から割り当てを変更できなくする。
(2) 特定の型の配列:配列の項で後述。
(3) オプション:設定しなくてもよいプロパティにできる。もしくはundefinedを指定可能。ここではtrue | undefined(合併型、後述)となる。
(4) インデックスシグネチャ:keyの型を指定できる。[key: T]: Uとすると「型Tのすべてのkeyは、型Uを持たなければならない」と強制できる。keyのTにはnumberかstringのみ指定可能。

合併型と交差型

合併型:集合でいうところの和集合。共用体型、共用型、Union型とも。Chicken | FishChickenか、Fishか、その両方となる。
交差型:積集合。Chicken & FishChickenとFishの両方となるので、合併型より狭い範囲の型となる。
今のところ交差型は使ったことない。合併型はたまに使う。

配列

さきほど出てきたstring[]のように、配列の要素の型を指定できる。できるというより指定すべき。指定しない場合は暗黙的に推論される。
そして要素の型はできるだけ少なく保つ。(string | number | boolean)[]ように複数指定できるが、型がごちゃごちゃしている配列なんて扱いづらい悪夢でしかない。

タプル

配列のサブタイプ。固定長の配列を型付けするための方法。これもコード見る方が早い。

let topping: [boolean, string, string] = [true, 'ajitama', 'nori']

topping = [true, 'menma', 'tyashu', 'horenso']

// 型 '[true, string, string, string]' を型 '[boolean, string, string]' に割り当てることはできません。
// ソースには 4 個の要素がありますが、ターゲットで使用できるのは 3 個のみです。


省略可能な要素、可変長の要素もサポートしている。

let memories: [number, number, number?] = [8, 16, 32]
memories = [8, 16]

let languages: [string, ...string[]] = ['TypeScript', 'Java', 'Rust', 'Go']


他、オブジェクトのプロパティ同様、readonly修飾子で読み取り専用配列にできる。読み取り専用にすると、要素の代入はもちろん、push()やsplice()といった破壊的メソッドが使用できなくなる(非破壊的メソッドは可能)。

let memories: readonly number[] = [8, 16, 32]

memories[0] = 4
// 型 'readonly number[]' のインデックス シグネチャは、読み取りのみを許可します。

memories.push(64)
// プロパティ 'push' は型 'readonly number[]' に存在しません。

console.log(memories.slice(0,2))    // [ 8, 16 ]


タプルはまだ活用できていないな。より配列を安全にできるので、使えるところでは使っていきたい。

null、undefined、void、never
意味
null 値の欠如
undefined 値がまだ割り当てられていない変数
void return文を持たない関数の戻り値
never 決して戻ることのない関数の戻り値

それぞれ概要は表のとおり。nullとundefinedはいつも仕事で苦労させられている。上手に制御しないといけない。
voidは、ただクラスのプロパティに値を代入しているだけ等の関数の戻り値に指定する。指定しなくても支障はない。
neverはあんまりみないな。例外をスローしている関数や、whileの無限ループしている関数等の戻り値。

最初に提示した図のように、unknownはすべての型のスーパータイプ、neverはすべての型のサブタイプ(ボトム型)。ボトム型はどこでも安全に使える。

列挙型

ある値について取り得る値を列挙(enumerate)する方法。以下のような形だが…

enum Kushikatu {
  Uzura = 'うずら',
  Pork = '豚肉',
  RedPickledGinger = '紅生姜',
  Price = 150
}

// 値の取得
console.log(Kushikatu.Uzura)        // うずら
console.log(Kushikatu['Price'])     // 150
console.log(Kushikatu[5])           // undefined!!!!


このように、コンパイル時にエラーが出ず、存在しない値にアクセスできてしまう。
これを避けるには、

const enum Kushikatu {
  Uzura = 'うずら',
  Pork = '豚肉',
  RedPickledGinger = '紅生姜',
  Price = '150'
}

console.log(Kushikatu[5])
// const 列挙型メンバーは、文字列リテラルを使用してのみアクセスできます。

const enumとし、文字列だけのenumとすること。
それかもしくは、そもそもenumを使用しないこと。
ちなみに、enumは同じ名前のenumがあると自動でマージする。interfaceと同様の挙動。



今回はここまで。これだけでも結構な文量になった。
他にもまとめたい事項がいっぱいある。大変だけど、書くと確実に頭に入るのでやっていこう。
次回は関数についての予定。