名前的型と構造的型の勘違いによる実話
addという関数があり、型Aの値同士を足せば型Aが返り、型Bの値同士を足せば形Bが返る、と宣言した
test.d.tsexport function add(x: A, y: A): A;
export function add(x: B, y: B): B;
しかし実際に使うとB同士を足した結果がAになってしまった、なぜか?
問題を再現する最小限のコード全体
test.tsimport { A, B, add } from "./testlib.js";
let x = new B();
let y = new B();
let z = add(x, y); // type of z is A
console.log(z);
testlib.d.tsexport class A {}
export class B {}
export function add(x: A, y: A): A;
export function add(x: B, y: B): B;
testlib.jsclass A{ }
class B{ }
function add(x, y) {
return x + y;
}
export {A, B, add}
解説
TypeScriptは名前的型システムではなく構造的型システム
なので構造が一致する型は同じ型
AとBは同じ構造なので同じ型
なので先に出現した export function add(x: A, y: A): A;
のルールが使われ、返り値がAとなる
AとBが区別されていないのでAとBをaddしてもエラーにならない
:let ab = add(new A(), new B()); // No error
解決方法
testlib.d.tsexport class A {
_A_Brand: never;
}
export class B {
_B_Brand: never;
}
これでAとBが異なる構造になるので型として区別される
この例では2つのクラスを区別したいシチュエーションだったので「使わないメンバーをつける」という選択をした。
文字列型を区別したいケースではメンバーを追加できないのでこの方法は使えない。
別の方法としてenumとのintersectionを作る方法がある。この場合は文字列型をFooId型にするのが as FooId
でできる。
tsenum FooIdBrand { _ = "" };
type FooId = FooIdBrand & string;
const fooId = 'foo' as FooId;
関連