Open6

TypeScript

thanaithanai

動機

TypeScriptというか、型を意識したプログラミング全般について、まだ理解できている(身に付いている)とは言い難いのでメモしておく。

プログラミングで型を使う意義や、型付けのお作法(いま現在それなりのSWEをしてる人ならおおむねこう書くよね的なもの)を、ある程度分かった状態になっておきたい。

題材

ちょうどAzureのREST APIを叩いてどうこうしたいものがあるので、それに沿って調べたことをメモしておく

thanaithanai

オブジェクトに型付けする

APIを叩いて返ってくるJSONにどう型付けするのが正解なのか。

  • 返ってくるJSONのうち利用する情報は一部だとして利用しない部分はどう扱うか
  • 状況によって含まれたり含まれなかったりするkeyをどう扱うか

オプショナルなプロパティの表し方

type Sample {
  id: string
  prop: string
  optionalProp?: string
}

typeとinterfaceの違い

このページが参考になる。

https://zenn.dev/luvmini511/articles/6c6f69481c2d17

interfaceは簡単に拡張可能なので知らないうちにプロパティが追加されているという罠が厄介そう。

この拡張可能な挙動のことをDeclaration mergingと呼ぶらしい。

https://zenn.dev/seya/articles/aa94166c977280

thanaithanai

mapでArray<Object>を返す

// これはNG
arr.map(e => { id: e.id });

// ()で囲うことでObjectであることを明示する
arr.map(e => ({ id: e.id }));
thanaithanai

型推論してくれないケース

素のjsから最低限エラーになりそうなところだけ型付けするみたいなのをやろうとした。

が、下のコードのforEachで型エラーが出なかったので、実行時エラーになった。
id: stringnumberが入るのを検知してエラーになってほしかったけど、そうならなかった)

// 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の時点でそれが子まで伝播するんじゃないか、という見解が多かった。

手がかりになりそうなの

この記事に気になる記述があった。

https://blog.uhy.ooo/entry/2021-10-23/how-you-use-typescript/

TypeScriptでは、型推論が働くためには関数引数の型を明示的に書く必要があります。これは推論力が足りないというよりは、TypeScriptがそのようなデザインを採用しているということです。関数型言語を中心に、引数の型を書かなくても関数の使われ方から推論してくれる言語もありますが、TypeScriptはそうではありません。おそらく、型チェック速度を遅くしないためというのが最も大きな理由でしょう。

なるほど~。

TypeScriptではこんなケースの型推論はされないよ!だってこういう仕組みだから!」みたいなのがまとまった文献が必要とされている(俺から)。

最近触ってるStandard MLだと型の記述しないで強力に型推論が効くから、そういうのが型付けプログラミングのスタンダードなのかと思ってたけど、どうやらそうではないらしい。

thanaithanai

オブジェクトのキーにユニオン型を使いたい

以下のエラーが出る。

インデックス シグネチャ パラメーターの型をリテラル型またはジェネリック型にすることはできません。代わりに、マップされたオブジェクト型の使用を検討してください。

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;
}
thanaithanai

クラスのプロパティ初期化子

コンストラクタに引数が不要な場合は、直接の代入がシンタックスシュガーになっているらしい。

class Sample {
  readonly number = 5
}