风味类型(可选符号属性的交叉)在基类型为复杂模板字面量时会失去区分能力
我正在使用 flavored types 模式(通过符号交叉实现的可选判别属性),并注意到当基类型是一个模板字面量且其中一个占位符包含联合类型时,它会悄悄失效。
const FLAVOR = Symbol("flavor");
// ✅ Works: number base
type VendorId = number & { [FLAVOR]?: "vendor" };
type MachineId = number & { [FLAVOR]?: "machine" };
declare const vendorId: VendorId;
declare function acceptMachineId(id: MachineId): void;
acceptMachineId(vendorId); // Error ✅
// ✅ Works: simple template literal base
type SimplePhone = `+${number}`;
type SimpleA = SimplePhone & { [FLAVOR]?: "a" };
type SimpleB = SimplePhone & { [FLAVOR]?: "b" };
declare const simpleB: SimpleB;
declare function acceptSimpleA(a: SimpleA): void;
acceptSimpleA(simpleB); // Error ✅
// ✅ Works: union base
type Language = "eng" | "deu" | "ita" | "fra";
type LanguageA = Language & { [FLAVOR]?: "a" };
type LanguageB = Language & { [FLAVOR]?: "b" };
declare const languageB: LanguageB;
declare function acceptLanguageA(a: LanguageA): void;
acceptLanguageA(languageB); // Error ✅
// ❌ Breaks: complex template literal with union in placeholder
type CountryCode = 1 | 33 | 44;
type PhoneNumber = `+${CountryCode}${number}${number}`;
type Whistleblower = PhoneNumber & { [FLAVOR]?: "whistleblower" };
type Relay = PhoneNumber & { [FLAVOR]?: "relay" };
declare const relay: Relay;
declare function acceptWhistleblower(w: Whistleblower): void;
acceptWhistleblower(relay); // No error ❌ — should be rejected
// ✅ Workaround: required (branded) property instead of optional
const BRAND = Symbol("brand");
type BrandedWhistleblower = PhoneNumber & { [BRAND]: "whistleblower" };
type BrandedRelay = PhoneNumber & { [BRAND]: "relay" };
declare const brandedRelay: BrandedRelay;
declare function acceptBrandedWhistleblower(w: BrandedWhistleblower): void;
acceptBrandedWhistleblower(brandedRelay); // Error ✅
我的问题: 为什么在模板字面量占位符中添加联合类型(例如 +${CountryCode}...,其中CountryCode = 1 | 33 | 44)会让两种不同风味的类型彼此可互相赋值?
解决方案
我提交了 microsoft/TypeScript#63420,并把这个问题的链接也引用了过去。看起来这个合并请求 microsoft/TypeScript#43440 修复了一个错误(参见 microsoft/TypeScript#43424),即模板字面量的检查曾经过于严格,但引入了如今这种检查过于宽松的行为。
这被归类为TypeScript的一个错误,分配给Backlog。这意味着TS团队没有立即修复的计划。如果有人想看到这个错误被修复,可能需要提交一个pull request来修复它(尽管不能保证会被接受)。
所以就问题本身的答案是:这是一个TypeScript的错误,可能会也可能不会被修复。
如果我想要绕过这个问题,我会在联合类型中添加一个非模板字面量成员,并尽量让该成员实际很少会被使用。比如,类型
interface Impossible { __absurd: never };
具有一个必需属性,其类型为 the never type。由于没有类型 never 的值,要得到类型 Impossible 的值就很困难(当然并非完全不可能,因为你可以做类似 { get __absurd(): never { throw new Error() } } 的操作,但我离题了)。无论如何,这给你带来类似于
type CountryCode = 1 | 33 | 44;
type PhoneNumber = `+${CountryCode}${number}${number}` | Impossible;
type Whistleblower = PhoneNumber & { [FLAVOR]?: "whistleblower" };
type Relay = PhoneNumber & { [FLAVOR]?: "relay" };
declare const relay: Relay;
declare function acceptWhistleblower(w: Whistleblower): void;
acceptWhistleblower(relay); // error, types of [FLAVOR] property are incompatible
现在大多数情况下应该按预期工作。当然,这只是一个权宜之计,还有很多其他的变通方法,因此遇到此问题的任何人都应结合自己的使用场景来判断,如有必要选择不同的方法。