社内勉強会の資料。
TypeScriptの型の考え方
TypeScriptの型は、「構造的部分型(Structural subtyping)」と呼ばれます。
Rubyなどの宣言的な型のない言語を書く人であれば、ダックタイピングだと言えば通じやすいと思います。
JS的に言えば、「とある型Aにとって必要なプロパティを所持しているオブジェクトは、その型Aであるとみなす」です。
type A = {
hoge: string;
piyo: number;
};
{ hoge: "" } // Aでない: 必要なプロパティ`piyo`が存在していない
{ hoge: "", piyo: 123 } // Aである: 必要なプロパティが両方ある
{ hoge: "", piyo: 123, fuga: "" } // Aである: 必要なプロパティが両方ある
これだけ。
ただし、これを使いやすくする、またプログラマのミスを事前に防ぐという目的で、いくつかの例外が存在しています。
TypeScriptの型の基本は構造的部分型ですが、より詳しく知るためにはこの構造的部分型の例外がいつどんな時に発生するのかを知る必要があります。
例外1: 余分なプロパティチェック(Excess Property Checking)
仕組み
type A = {
hoge: string;
piyo: number;
};
let o1: A;
const o2 = { hoge: "", piyo: 123, foo: "foo" };
o1 = o2; // OK
o1 = { hoge: "", piyo: 123, foo: "foo" }; // Error: Object literal may only specify known properties, and 'foo' does not exist in type 'A'.ts(2322)
さて、o1
はA
の型エイリアスを当てていて、o2
は型の宣言はなしなので、オブジェクトのリテラル型です。
o1 = o2;
がOKなのは、構造的部分型であることを考えれば違和感ないと思います。
しかし、最終行は以下のエラーが出てコンパイルが通りません。
Object literal may only specify known properties, and 'foo' does not exist in type 'A'.ts(2322)
構造的部分型であるにも関わらず、余分なプロパティを持っていることを怒られています。
第一の例外はこの「余分なプロパティチェック(Excess Property Checking)」で、エラーの通り、リテラルの代入時は型チェックが厳しくなります。
これは、ライブラリのオプションなどを指定する場合に、タイポなどで存在しないオプションを渡してしまっている場合にすぐに気づけるための仕組みです。
例
type Option = {
hogeable?: boolean;
piyoName: string;
};
type createHoge = (option: Option) => Hoge;
のような型定義のライブラリがあったとして
const hoge = createHoge({ piyoName: "PIYO" });
これは通ります。hogeable
はオプション(?
付き)なので、Option
に必要な型はpiyoName
だけだからです。
しかし、
// hogeable を hogableにタイポしている!
const hoge = createHoge({ hogable: true, piyoName: "PIYO" });
構造的部分型であれば、必要な型さえあれば他にどんなプロパティを持っていたとしても問題ないはずですが、これは上述の通りエラーになります。
Object literal may only specify known properties, but 'hogable' does not exist in type 'Option'. Did you mean to write 'hogeable'?ts(2322)
例外2: 弱い型(Weak Type)
仕組み
Weak TypeはTypeScript2.4で導入されました。
Weak Typeとは、全てのプロパティがオプションであるオブジェクト型のことを指します。
type A = {
hoge?: string;
piyo?: number;
};
さて、構造的部分型で考えるのであれば、この型には必要なプロパティが存在しないので、全てのオブジェクトが代入可能なはずです。
ちなみに、ここで言う「全てのオブジェクト」 というのは、オブジェクトではないnumberリテラルやstringリテラルなども含みます。
これらのリテラルにはAuto Boxingが効く("hoge".length
ができる理由)ので、オブジェクトとして扱われます。
つまり、{}
型がnull
とundefined
以外の値全てを代入可能なように、上記のtyep A
もnull
とundefined
以外の全ての値を代入可能ということです。
オブジェクトの型を定義しているのにnull
とundefined
以外なんでも入るって、型の定義として意味がなさすぎるので、それを弾く仕組みとしてWeak Typeが導入されました。
Weak Typeにおいては、必ずどれか一つのプロパティを持っているオブジェクト(typeof
で"object"
が返ってくる値)しか代入できません。
さらに、このルールはリテラルであっても変数であっても適用されます。
例
試してみましょう。
まずはWeak Typeが存在していない世界線の話。
Weak Typeは2.4.0で導入されているので、今回はWeak Typeが存在しないTypeScript2.0.0を使用します。
$ npm install --save-dev typescript@2.0.0
$ npx tsc -v
Version 2.0.0
type A = {
hoge?: string;
piyo?: string;
};
const val1 = { fuga: 123 };
const val2 = 123;
const val3 = () => {};
let result: A;
result = val1;
result = val2;
result = val3;
こんなコードを用意しました。
type A
はさっきの通り、そしてA
型のresult
に対して色々代入します。
これをコンパイルすると...
$ ls -l -1
index.ts
node_modules
package-lock.json
package.json
# コンパイル実行
$ npx tsc index.ts
# 問題なく通ってる
$ ls -l -1
index.js # <- 追加されてる
index.ts
node_modules
package-lock.json
package.json
あんれまあ。
普通に通ってしまいます。
まさに構造的部分型なわけですが、オブジェクトの型を定義してるのにnumberリテラルとかオブジェクト以外のもの入れて欲しくないですよね
\そこでWeak Typeの登場です/
次は、TypeScript2.6.1で試します。
(2.4で導入されているのになんで2.6なんやって感じですが、ちょくちょくWeak Type自体にアップデートがかかっていて、2.4.1で試したら普通にリテラル代入できちゃいました😇例にならないので関数も弾いてくれる2.6.1で)
$ npm uninstall typepscript
$ npm install --save-dev TypeScript@2.6.1
$ npx tsc -v
Version 2.6.1
$ npx tsc index.ts
index.ts(12,1): error TS2559: Type '{ fuga: number; }' has no properties in common with type 'A'.
index.ts(13,1): error TS2559: Type '123' has no properties in common with type 'A'.
index.ts(14,1): error TS2559: Type '() => void' has no properties in common with type 'A'.
ということで、A
に対しての代入がこのようにエラーになります。
通すためには、
type A = {
hoge?: string;
piyo?: string;
};
// hogeを含んでいる
const val1 = { hoge: "", fuga: 123 };
let result: A;
result = val1;
このように一つ以上のプロパティを持っている必要があります。
リテラルの代入はもちろんのこと、Weak Typeの型チェック時には変数に代入していても上記のようにエラーになります。
理由を知っていれば納得できますね。
Excess Property CheckingとWeak Type、挙動似てない?
ちょろっと説明読んだだけだと、この二つの違いは何...ってなるんですが、導入された理由を知っておくと理解が進むと思います。
ていうか、公式のExcess Property Checkingの例のコードでWeak Typeになる型定義をしているのがいけない気がする(と思ってこの例ではExcess Property Checkingの例のコードで非Weak Typeな型定義をしてる)...。
TODO
- Weak Typeのエラーの他の回避の方法について