TypeScript
動機
TypeScriptというか、型を意識したプログラミング全般について、まだ理解できている(身に付いている)とは言い難いのでメモしておく。
プログラミングで型を使う意義や、型付けのお作法(いま現在それなりのSWEをしてる人ならおおむねこう書くよね的なもの)を、ある程度分かった状態になっておきたい。
題材
ちょうどAzureのREST APIを叩いてどうこうしたいものがあるので、それに沿って調べたことをメモしておく
オブジェクトに型付けする
APIを叩いて返ってくるJSONにどう型付けするのが正解なのか。
- 返ってくるJSONのうち利用する情報は一部だとして利用しない部分はどう扱うか
- 状況によって含まれたり含まれなかったりするkeyをどう扱うか
オプショナルなプロパティの表し方
type Sample {
id: string
prop: string
optionalProp?: string
}
typeとinterfaceの違い
このページが参考になる。
interface
は簡単に拡張可能なので知らないうちにプロパティが追加されているという罠が厄介そう。
この拡張可能な挙動のことをDeclaration mergingと呼ぶらしい。
mapでArray<Object>を返す
// これはNG
arr.map(e => { id: e.id });
// ()で囲うことでObjectであることを明示する
arr.map(e => ({ id: e.id }));
型推論してくれないケース
素のjsから最低限エラーになりそうなところだけ型付けするみたいなのをやろうとした。
が、下のコードのforEach
で型エラーが出なかったので、実行時エラーになった。
(id: string
にnumber
が入るのを検知してエラーになってほしかったけど、そうならなかった)
// APIから返ってくるJSONの型付けをいったん後回しにした
const queriedObj = getQueriedObj();
// 使う部分のプロパティだけ型定義した
interface MyObj {
id: number
// other props...
}
// mapで受け取る各要素は型を指定した(queriedObj.somePropはMyObj[]型)
const ids = queriedObj.someProp.map((item: MyObj) => item.id);
// numberをstringとして取り扱おうとしているがエラーにならない
ids.forEach((id: string) => {
// do something
});
map
の部分で各要素はMyObj
だと指定しているのでid
の型推論が出来てもいい気がするけど、ダメっぽい。
(const ids: number[] = ...
と書けば怒ってくれたけど、こっちは推論できて上の記述だと推論できない理由がよくわからない)
型推論が効いたケース
// idsの型を明示する
const ids: number[] = queriedObj.someProp.map((item: MyObj) => item.id);
// queriedObj.somePropの型を明示する
const targetArray: MyObj[] = queriedObj.someProp;
const ids = targetArray.map((item: MyObj) => item.id);
親となる要素の型が確定してないとちゃんと推論されない?
何人かに聞いたところ、どうも親がany
の時点でそれが子まで伝播するんじゃないか、という見解が多かった。
手がかりになりそうなの
この記事に気になる記述があった。
TypeScriptでは、型推論が働くためには関数引数の型を明示的に書く必要があります。これは推論力が足りないというよりは、TypeScriptがそのようなデザインを採用しているということです。関数型言語を中心に、引数の型を書かなくても関数の使われ方から推論してくれる言語もありますが、TypeScriptはそうではありません。おそらく、型チェック速度を遅くしないためというのが最も大きな理由でしょう。
なるほど~。
「TypeScriptではこんなケースの型推論はされないよ!だってこういう仕組みだから!」みたいなのがまとまった文献が必要とされている(俺から)。
最近触ってるStandard MLだと型の記述しないで強力に型推論が効くから、そういうのが型付けプログラミングのスタンダードなのかと思ってたけど、どうやらそうではないらしい。
オブジェクトのキーにユニオン型を使いたい
以下のエラーが出る。
インデックス シグネチャ パラメーターの型をリテラル型またはジェネリック型にすることはできません。代わりに、マップされたオブジェクト型の使用を検討してください。
type MyObjKey = 'id' | 'name' | 'age';
interface MyObj {
[key: MyObjKey]: string;
}
「マップされたオブジェクト型」を使おうとする
違うエラーが出る。
マップされた型では、プロパティまたはメソッドを宣言しない場合があります。
type MyObjKey = 'id' | 'name' | 'age';
interface MyObj {
[key in MyObjKey]: string;
}
なんでー。どうすればいいのー。
typeだといけるよと教えてもらう
Twitterで教えてもろた。なぜかtype
を使うと通る。どういう内部的な違いがあるのかまだ理解できていない。
type MyObjKey = 'id' | 'name' | 'age';
type MyObj = {
[key in MyObjKey]: string;
}
クラスのプロパティ初期化子
コンストラクタに引数が不要な場合は、直接の代入がシンタックスシュガーになっているらしい。
class Sample {
readonly number = 5
}