名前的型と構造的型の勘違いによる実話
addという関数があり、型Aの値同士を足せば型Aが返り、型Bの値同士を足せば形Bが返る、と宣言した
code:test.d.ts
export function add(x: A, y: A): A;
export function add(x: B, y: B): B;
しかし実際に使うとB同士を足した結果がAになってしまった、なぜか?
https://gyazo.com/fc1d9dca9ea1ceedf5575090dc7d119f
問題を再現する最小限のコード全体
code: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);
code: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;
code: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してもエラーにならない
code::
let ab = add(new A(), new B()); // No error
解決方法
code:testlib.d.ts
export class A {
_A_Brand: never;
}
export class B {
_B_Brand: never;
}
これでAとBが異なる構造になるので型として区別される
https://gyazo.com/7d13e82f4439f5fece41d74ac85a3224
この例では2つのクラスを区別したいシチュエーションだったので「使わないメンバーをつける」という選択をした。
文字列型を区別したいケースではメンバーを追加できないのでこの方法は使えない。
別の方法としてenumとのintersectionを作る方法がある。この場合は文字列型をFooId型にするのがas FooIdでできる。
code:ts
enum FooIdBrand { _ = "" };
type FooId = FooIdBrand & string;
const fooId = 'foo' as FooId;
関連