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になってしまった、なぜか?

image

問題を再現する最小限のコード全体 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が異なる構造になるので型として区別される image

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

enum FooIdBrand { _ = "" };
type FooId = FooIdBrand & string;
const fooId = 'foo' as FooId;

関連