こんにちは、びしょ〜じょです。 最近は脳を全く使っておらずなんたらかんたら。

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.

ではこちらをご覧ください。

TypeScript Version: Whatever is running on TypeScript Playground Code var o1: { p: string, q: number } = { p: "", q: 0 }; var o2: { p: string } = o1; var o3: { p: string, q: string } = { ...

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 -> ST を渡すと(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 です。 stealSecretObjWithSecret<T> を返すために、戻り値のrecordに secret: '' を渡しています。 spread syntaxによって o の中身がぶち撒かれるのですが、ここで osecret を持っていると(!) 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やめたいけどウェブブラウザの目が黒いうちは…



  1. Siek, Jeremy and Taha, Walid. "Gradual Typing for Objects". 

  2. 大堀淳, "新装版 プログラミング言語の基礎理論" ほか