<書評> プロを目指す人のためのTypeScript入門 安全なコードの書き方から高度な型の使い方まで

published_at: 2022-04-29

要約

TSの基礎的な内容が割と網羅されている印象。 抜けていた基礎知識を補えたのが良かった。

1章

TSバージョンの年表が面白かった印象。 TS 4.1から応用性の高いtemplate literal typesが導入されてお祭り騒ぎになったらしい。元PRはこちら。 template literal typesってinferと組み合わせると有用だなと思ったり。

あとはenum使わない方が良いみたいなのは知らなかった。JSには存在しない独自機能だからとのこと。

LINE社の記事を見ると、 JSに存在しないenumを表現するためにIIFE(即時実行関数)を含んだコードをトランスパイルするらしい。それがバンドルされてしまうからtree-shakingできなくなるよとのこと。なるほど。

2章

値がない状況を表すのにundefinedの方が推奨されるとのこと。

MicrosoftのTSコーディング規約にも **Use undefined. Do not use null. **の記載がある。

サバイバルTypeScriptにはこのような記述がある

  • undefinedは「値が代入されていないため、値がない」nullは「代入すべき値が存在しないため、値がない」
  • nullは自然発生しない
  • undefinedは変数
  • undefinedはtypeofの結果がプリミティブ名を表す"undefined"になるのに対し、nullは"null"ではなく"object"になります。

最後のは割とびっくりする挙動。言語仕様的にundefinedが自然発生してくれるのでそれに合わせにいくのが良さそう。

ただ外部APIとのやり取りでnullが渡ってくるのはあると思うので、そこの考慮が必要そう。

Null 合体演算子 (??) 論理 OR 演算子 (||)  の違いを知った。 || はnullやundefinedだけでなく空文字や0、falseなども "ない"と評価して右辺値を返すが、?? はnull orundefinedの場合のみ右辺値を返す。

ES2021の新機能として ||=(論理和代入) ****&&=(論理積代入)演算子もある。

論理和代入はrubyの自己代入演算子と同じ。論理積代入は左辺がtruthyなものの場合に右辺の値を左辺に代入。

3章

スプレッド構文とシャローコピー

スプレッド構文はシャローコピーであること。 下記よりスプレッド構文でコピーしてもネストしたオブジェクトは同じアドレスを共有している。

1const object = { name: 'test', type: { a: 'a' } }; 2const anotherObject = { ...object }; 3object.name = 'object'; 4anotherObject.type.a = 'b'; 5 6console.log('object', object); 7// object { name: 'object', type: {a: 'b'} } 8console.log('anotherObject', anotherObject); 9// anotherObject { name: 'test', type: {a: 'b'} }

ディープコピーをする方法の1つとして、JSON.stringify() でオブジェクトを JSON 文字列に変換し、 JSON.parse() で文字列から(完全に新しい) Javascript のオブジェクトに変換する方法がある。 また、ネストしたオブジェクトもスプレッド構文で展開する等々。

インデックスシグネチャについて

インデックスシグネチャは型安全性を破壊する。 オブジェクトのkeyがstringの場合、obj.key でアクセスできるが、obj.foo / obj.aなど任意のkeyにもアクセスできてコンパイルエラーにならないので型安全ではない。

1type Obj = { [key: string]: number }; 2const obj: Obj = { foo: 1 }; 3 4console.log(obj.t); 5// => コンパイルエラーにならない

部分型関係について

型Sが型Tの部分型であるとは、S型の値がT型の値でもあることを指す。 下記の例ではFooBarBaz がFooBar の部分型である。 FooBarBazはFooBarの要素を全て持つ。FooBarBaz型であればFooBar型でもあるということ。

1type FooBar = { foo: string; bar: number }; 2type FooBarBaz = { foo: string; bar: number; baz: boolean }; 3const obj: FooBarBaz = { foo: 'hi', bar: 1, baz: false }; 4 5console.log('obj', obj); 6// "obj",{"foo":"hi","bar":1,"baz":false} 7 8const obj2: FooBar = obj; 9console.log('obj2', obj2); 10// "obj2",{"foo":"hi","bar":1,"baz":false}

タプル型

要素数が固定された配列型。 要素数が固定されている代わりに、それぞれの要素に異なる型を与えられる。

オブジェクトの分割代入(destructuring assignment)について

型注釈つけられないで型推論していく

プリミティブのプロパティ

文字列や数値などのプリミティブ型は、プロパティを持ったオブジェクトとして扱える。

プリミティブ型をまるでobjectのように扱えるのはJSの特徴。JSには、プリミティブ型をオブジェクトに自動変換するオートボクシングと呼ばれる機能がある。参考: https://typescriptbook.jp/reference/values-types-variables/primitive-types

1'name'.length; // 4

4章

contextual typing

逆方向の方推論のことで、型が事前に分かっている場合の推論。 通常の型推論は型注釈は無いけれど値から型を推論できる場合に発動

1const xRepeat = (arg: number): string => 'x'.repeat(arg); 2// xRepeatの型が自動的にconst xRepeat: (arg: number) => string と判定される

逆方向の型推論は型を事前に割り当てているから式中の型を省略できる。

1type F = (arg: number) => string; 2const xRepeat: F = (arg) => 'x'.repeat(arg); 3// (arg: number): string の指定は不要

コールバック関数なども指揮中で引数の型を書かない場合が多い。 例えばfilterなどはレシーバを元に型引数が決まる。

1const nums = [1, 2, 3]; 2const ary = nums.filter((x) => x > 2); 3// 式中xの変数をnumberとしなくても x>2 でコンパイルエラーが起きない 4// (value: number) => unkhown

5章

protectedはクラス自身だけでなく子クラスからもアクセス可能。rubyと同じだな。

プライベートプロパティについて

#プロパティ名 と private修飾子をつけたもの両方で表現可能。

  • privateはTS独自機能なのでトランスパイル後のランタイム上では普通のプロパティとして扱う。
  • #プロパティ名はJSの機能なのでランタイム上でもプライベート性が担保される

this

thisはプロパティを参照することが多く、そのプロパティがundefinedであった場合にランタイムエラーが発生する可能性あり。

  • アロー関数は外側のthisを引き継ぐ
  • 普通の関数は呼び出し時のレシーバをthisとする

アロー関数への書き換えをするリファクタリングを業務で行ったなあ。

6章

インターセクション型

T型でありかつU型でもある値を意味する型 参考: https://typescriptbook.jp/reference/values-types-variables/intersection ちなみに string & numberのプリミティブのインターセクション型は存在しないのでnever返る

Optional chaining

レシーバに対する参照が nullish (null または undefined) の場合にエラーとなるのではなく、式が短絡され undefined が返される。 ?.はそれ以降のプロパティアクセス・関数呼び出し・メソッド呼び出しをまとめて飛ばす効果を持つ。

1const adventurer = { name: 'Alice', cat: { name: 'Dinah' } }; 2const dogName = adventurer.dog?.name.t; 3console.log(dogName); 4// expected output: undefined 5// dogが存在しないのでそれ以降の .name.t は評価されずにundefinedが返る

リテラル型のwidening

リテラルが自動的に対応するプリミティブ型に変化する(広げられる)特徴。

constの場合はリテラル型なのにletの場合はプリミティブ型に変化している。

letは再代入が期待されるのでプリミティブ型に変化する。

ちなみにconstで宣言したオブジェクトリテラルも再代入可能なのでwideningされる。

1const test1 = 'test'; 2// const test1: "test" 3 4let test2 = 'test'; 5// let test2: string 6 7const test = { a: 'a', b: 1 }; 8// const test: { a: string; b: number; }

wideningを防ぎたい場合は、リテラル型を含む型注釈をつけるか as constを利用して宣言する。

ユニオン型

型の絞り込みに対応している点が素晴らしいとのこと。なるほど、そういう解釈ができるのか。

関数の引数をユニオン型で取って、式中で型の分岐を行つつ処理をしていけるということに価値があるのか。

lookup型

T[K] のようにオブジェクトプロパティにアクセスする際の型

as const

asを使った型アサーションは型安全性を破壊しかねないけれど、as constはそれとは異なる。

  1. 配列リテラルの型推論結果を配列型ではなくタプル型にする
  2. オブジェクトリテラルから推論されるオブジェクト型は全てreadonlyになる(配列リテラルも)
  3. プリミティブのリテラル型がwideningしない
  4. テンプレート文字列リテラルの方がstringではなくテンプレートリテラル型になる
1// readonly 2const names = ['taro', 'jiro'] as const; 3// const names: readonly ["taro", "jiro"] 4 5const obj = { name: 'test', address: { city: 'xxxx' } } as const; 6// const obj: { 7// readonly name: "test"; 8// readonly address: { 9// readonly city: "xxxx"; 10// }; 11// } 12// wideningされない 13 14const name1 = 'name' as const; 15let name2 = name1; 16let name2: 'name'; 17// let name2: "name" 18 19let name3 = 'name3'; 20// let name3: string 21// templateliteral type 22 23const n: number = 1; 24const value = `${n}px` as const; 25// const value: `${number}px`

any型とunkhown型

  • any型
    • 型チェックを無効化する型。基本的に何でも代入可能なのでコンパイルエラーにならない。
    • JSからTSへの移行を支援するためや、型をうまく表現できないためのエスケープハッチとしての存在理由あり
    • anyよりasやユーザー定義型ガードの利用を検討した方が良い
  • unkhown型
    • なんでも入れられる型。何かは入っているが何かは分からないような状況を表す
    • anyはコンパイルエラーにならないので、val.nameなどにアクセスできるがunkhownはできない。TSコンパイラがunkhownは値がないことを読み取ってできることを制限してくれる。

never型

値を持たないことを表す型 never型には何も代入できない。(never型にnever型は代入可能)

他の型にnever型は代入できる。そのため、TSではbottom型と呼ばれている。(取りうる値の集合が一番大きい => ボトム型は全ての型の部分型)

voidとneverの違い

  • voidはundefinedを代入できるがneverはできない(値を持たないから)
  • どちらも戻り値がないことは同じ
    • voidは関数がreturnされるか、最後まで実行されることを表す
    • neverは中断されるか永遠に実行される

ユーザー定義型ガード

型の絞り込みの一種 引数名 is 型名 という形で引数を型名で絞り込んで処理する。 下記の関数は戻り値がbooleanであり、trueならば引数がstring or numberであるということを意味する。

1function isStringOrNumber(value: unknown): value is string | number { 2 return typeof value === 'string' || typeof value === 'number'; 3} 4 5function useUnknown(v: unknown) { 6 // v はここでは unknown 型 7 if (isStringOrNumber(v)) { 8 // v はここでは string | number 型 9 console.log(v.toString()); 10 } 11}

参考: https://blog.uhy.ooo/entry/2021-04-09/typescript-is-any-as/ asserts 引数名 is 型名 という型述語もある。 この場合は戻り値がvoidで「関数が無事に終了すれば引数名は型名である」

union distribution

conditional typesの性質 conditinal typesを利用している型にユニオン型を渡すと分配して処理してくれる 公式 のやつコピペしただけだけどこんな感じになる。

1type ToArray<Type> = Type extends any ? Type[] : never; 2 3type StrArrOrNumArr = ToArray<string | number>; 4// ToArray<string> | ToArray<number>; と等しいので、string[] | numbe[]が返る

7章

commonJS

nodejsで利用されているソースコードをパッケージ化するモジュール。 最近はECMAScriptのESmodule形式も採用しているよう。 requireを利用してimportする。

1const circle = require('./circle.js'); 2console.log(`The area of a circle of radius 4 is ${circle.area(4)}`);

今後

(本書付録2の更なる学習の道しるべコピペしただけ)

JS言語機能

  • イテレータ・ジェネレータ
  • メタプロ系
    • オブジェクトの操作(Object.keys)など
    • プロパティディスクリプタ・プロパティ属性
    • Reflect・Proxy
  • シンボル
  • prototype

TSの言語機能

  • 標準ライブラリに属する他の型(Record・Parameters・Awaitedなど)
  • abstractクラス・abstract new シグネチャ
  • 型レベルプログラミング
  • mapped types conditionaltypes
    • infer / union distribution / homomophic typesなど