はじめに
みなさんEffective Typescriptはもう読まれましたか?この本はTypeScriptの重箱つつきみたいな事しているなと最初から読んでいくと疲れてしまう本ですが、自分の興味あるところから読んでいくとコードの例からTypeScriptの特徴と、それに合わせたベストプラクティスが学べて、なかなかおもしろいですよ。
例えばこんなことを私は今日学びました。
コードを最初に示します
type Pair = [number, number];
const list = [1, 2];
const tuple: Pair = list; // this code make an error
type PairObject= {a:number, b:number}
const object = {a:1, b:2, c:3}
const pairObject: PairObject = object;
上のArrayのコードは型を指定して代入しない限りエラーを起こします。
const list = [1, 2]をTypeScriptは number[]と解釈するので、2つのnumberをもつ配列に代出来ないと判断するのです。
ところが下のオブジェクトは違います。
代入すべきオブジェクトが型指定して2つの値を持つことを指定しても、問題なく3つの値が代入出来ています。PairObjectは最低限a, bの2つのnumberを持つことを保証するだけで、その他にc:numberがあっても問題ないと判断するのです。
この問題の最も簡単な解決方法
type Pair = [number, number];
const list: Pair = [1, 2];
const tuple: Pair = list;
type PairObject= {a:number, b:number}
const object:PairObject = {a:1, b:2, c:3} # ここでエラーが発生
const pairObject: PairObject = object;
代入する変数の定義に型推論を行わずに、型指定をすることです。これで、[1, 2]という変数がnumber[]型と推測されるのが防がれて、PairObjectにcという余分なプロパティがあることがチェックされます。
代入して型推測するほうが簡単では?
この問題は配列やオブジェクトを代入して型推測させれば一見、解決しそうに思えます。でも問題はそう単純じゃないんです。
const list2 = [1, 2];
type PairType = typeof list2;
const list3: PairType = [3, 4];
const list4: PairType = [1, 2, 3, 4]; // これはエラーにならない
const PairObject = {a:1, b:2};
type PairObjectType = typeof PairObject;
const pairObject2: PairObjectType = {a:3, b:4};
const pairObject3: PairObjectType = {a:1, b:2, c:3}; // これはエラーになる
この通り、当たり前ですが、今度は配列に何個でも代入出来るようになってしまいます。
as constを使ったら
さてTypeScriptにはas constという便利な修飾子があります。それを使用すれば問題は解決するように思えますが。
値の後にas constと書くと、TypeScriptはその値について可能なかぎり狭い型を推論します。型の拡大は起こりません。真に不変の定数では、通常これが必要になります。また、配列でas constを使用するとタプル型が推論されます。
[Effective Typescript より引用]
const list5 = [1, 2] as const;
type PairType2 = typeof list5;
const list6: PairType2 = [3, 4]; // これもエラーになる
const list7: PairType2 = [1, 2, 3, 4]; // これもエラー
const PairObject2 = { a: 1, b: 2 };
type PairObjectType2 = typeof PairObject2 ;
const pairObject5: PairObjectType2 = { a: 3, b: 4 };
const pairObject6: PairObjectType2 = { a: 1, b: 2, c: 3 }; // これはエラーになる
この通り、エラーが増えます。as constを使うと、readonly [1, 2]というリテラル型になるため、
オブジェクトを常に型定義して使うのが面倒なら
const testFunc =({a, b}: {a:number, b:number}) => {
return a + b;
}
// ✅ OK: 変数経由だとエラーにならない
const object4 = {a:1, b:2, c:3};
testFunc(object4);
// ❌ エラー: オブジェクトリテラルを直接渡すとエラー
testFunc({a:1, b:2, c:3}); // Object literal may only specify known properties
このようにfunctionのコールの中にobjectを書けるが、長いオブジェクトをFunctionCallに入れられると読みづらい。
結論
- 長さの決まったArrayを作りたければ型指定しないといけない。
- 型指定のないオブジェクトを安易に作らない
- 複数箇所で使う型は定義して再利用する