Unsound TypeScript: spread syntax
こんにちは、びしょ〜じょです。 最近は脳を全く使っておらずなんたらかんたら。
Promise
の話はなんかムリそうだったので、今度は勘ではなく文献等にあたって実装するかもしれません。
でもそれってオレやる必要ある?
とにかく今回は別の話をします。
はじめに
TypeScriptの利用を積極的に避けている人、賢い。
TypeScriptは漸進的型付けを採用しており、よくわからない型に any
を付けることができます。
そしてTypeScriptは(gradural) subtypingを採用しており、さらに any
があらゆる型のtop typeとなっております。
このことは、文献[1]を読んでいただくと分かるとおり(読むまでもなく直感的にも分かりますが)、型システムはunsoundになります。
const x: string = "hoge";
// string <: any
const y: any = x;
type T = { t: () => number };
const strip = (t: T) => t.t;
// T <: any
// typed but raising RTE
console.log(strip(y)());
any
は嘘とかなんとかありますが、とにかく人間に気をつけさせる運用になり、typed languagesの世界に住んでいる人々が発狂して明日から職場に来なくなるので絶対にやめましょう。
Gradual typing + subtypingでは星型をobjectのtop typeにするとどうなるかを文献[1]に基づいて解説しようと思ったのですが、上に示したとおり自明ではあり、面白い場所まで踏み込むには今は私のプログラミン能力が低下してるのであきらめます。
spread syntax is unsound
続きましてはstructural subtypingとspread syntaxです。
any
は一旦置いておきます。 any
のことは忘れましたか? OK.
ではこちらをご覧ください。
2017年に報告されているものですが、TSの最新バージョンの4.02でも依然再現できます。
一般的なstructural subtypingにおいては、一度upcastすると型情報が一部失われてしまう、という問題があります。
例えば、あるレコード型 T = { l1: L1, l2: L2, ..., ln: Ln }
があって T <: S where S = { l1: L1, ..., lm: Lm } (m < n)
という関係があるときに、 S
上の恒等関数 idS : S -> S
に T
を渡すと(subtyping relation により渡せる)、 idS
の返す値のプロパティ l(m+1)
、 ...、 ln
が参照できなくなる、または idS
の中で T
の型情報を一部損失してしまう[2]。
しかし、いくらかの(あるいは多くの)人々は、この問題をうまいこと制約付として利用しています。 つまり、ある関数にオブジェクトを渡すなどすると、不要な情報を排除し、使われてほしい情報だけを持つ(参照できる)ようなオブジェクトに生まれ変わらせる、という使い方です。
ところが、上記のissueによれば、TSは型情報の(またはruntime valueの)損失が無いようです。 structural subtypingの問題点を克服しました! すばらしいですね!
じゃあ問題点ではなく性質として利用していた人はどうなる? そもそも持ってない(失われた)プロパティの値が湧き出てくるのはそれこそ問題じゃないですか?
type Obj<T> = { t: T };
type Secret = string;
type ObjWithSecret<T> = Obj<T> & { secret: Secret };
const o: ObjWithSecret<number> = { t: 3, secret: "secret" }
// const u: Obj<number> = { t: o.t, secret: o.secret } // *TS 4.0 以降は* type error
const u: Obj<number> = { ...o };
const stealSecret = <T>(o: Obj<T>): ObjWithSecret<T> => ({ secret: '', ...o });
console.log(stealSecret(u).secret);
何か t: T
なるラベルを持つレコード Obj<T>
、 および type ObjWithSecret<T> = {t: T, secret: Secret}
を定義しました。
o: ObiWithSecret<number>
を定義してから、 u: Obj<number>
を spread syntax で定義します。
このとき u
の 値は {t: 3, secret: "secret"}
となり、本来持っていてほしくない secret
も持っています。
まあ 型は Obj<number>
なので参照できなければいいでしょ、とそれなりの妥協や納得があります。
しかし、 as any
などのメチャクチャな型の操作をおこなわずとも値を参照できたら…どうする…。
注目すべきは stealSecret
です。
stealSecret
が ObjWithSecret<T>
を返すために、戻り値のrecordに secret: ''
を渡しています。
spread syntaxによって o
の中身がぶち撒かれるのですが、ここで o
が secret
を持っていると(!) secret
の値を上書きします。
これで無事 secret
をリークすることに成功しました。
問題っぽいですねえ。
というのを書いてから実際にgo wrongする例を思いついたんでぺたり。
type T = { y: number };
type U = { y: () => number };
const t: T = { y: 5 };
const m: {} = { ...t };
const u: U = { y: () => 0, ...m };
u.y();
空のレコード m: {}
として { ...t }
を定義してから、 U
型の値に m
の中身をぶちまけた結果、 y: () => number
の 値が上書きされてしまいました。
JSでRTEするなら、概ねの演算子はだいたい NaN
になるしdot accessは undefined
を返すので、関数呼出しが簡単でオススメです。
おわりに
TypeScript、というかJSやめたいけどウェブブラウザの目が黒いうちは…
-
Siek, Jeremy and Taha, Walid. "Gradual Typing for Objects". ↩
-
大堀淳, "新装版 プログラミング言語の基礎理論" ほか ↩