generated at
名前的型と構造的型の勘違いによる実話
addという関数があり、型Aの値同士を足せば型Aが返り、型Bの値同士を足せば形Bが返る、と宣言した
test.d.ts
export function add(x: A, y: A): A; export function add(x: B, y: B): B;
しかし実際に使うとB同士を足した結果がAになってしまった、なぜか?


問題を再現する最小限のコード全体
test.ts
import { 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.ts
export class A {} export class B {} export function add(x: A, y: A): A; export function add(x: B, y: B): B;

testlib.js
class 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.ts
export class A { _A_Brand: never; } export class B { _B_Brand: never; }
これでAとBが異なる構造になるので型として区別される

この例では2つのクラスを区別したいシチュエーションだったので「使わないメンバーをつける」という選択をした。
文字列型を区別したいケースではメンバーを追加できないのでこの方法は使えない。
別の方法としてenumとのintersectionを作る方法がある。この場合は文字列型をFooId型にするのが as FooId でできる。
ts
enum FooIdBrand { _ = "" }; type FooId = FooIdBrand & string; const fooId = 'foo' as FooId;

関連