为什么 template literal 包装 branded string 后不再兼容?
数据小陈
结论
brandStringTest1 和 brandStringTest2 不是可互换的类型。前者是字符串模板类型与对象类型的交叉,后者则把整个交叉类型放进 template literal 的占位符中。TypeScript 不会因此保证原有字符串模式和交叉条件都按预期展开。因此,'/test/abc' 可以赋给前者,却不能赋给后者。
两种定义表达的条件不同
type brandStringTest1 = `/test/${string}` & { __brand?: never }
type brandStringTest2 = `${brandStringTest1}`
brandStringTest1 要求值符合 /test/${string},并满足 { __brand?: never }。这个对象属性是可选的,所以普通字符串字面量即使没有提供该属性,也不会因此被拒绝。
brandStringTest2 的占位符是完整的 brandStringTest1 交叉类型。它不等同于重新声明 /test/${string},外层的 template literal 也不会自动让它与 brandStringTest1 成为同一种类型。报错中的目标类型 `${brandStringTest1}` 就体现了这一差异。
问题出在类型表达式的结构和赋值检查上,字符串值在运行时不会被包装或改变。
保留原有约束的写法
如果两个别名应该接受同一批字符串,可以先定义字符串模式,再与可选标记交叉:
type TestPath = `/test/${string}`
type BrandMarker = { __brand?: never }
type brandStringTest1 = TestPath & BrandMarker
type brandStringTest2 = TestPath & BrandMarker
const testTest_1 = (arg: brandStringTest1) => arg
const testRes1 = testTest_1('/test/abc')
const testTest_2 = (arg: brandStringTest2) => arg
const testRes2 = testTest_2('/test/abc')
如果不需要转换类型,直接复用已有定义即可:
type brandStringTest2 = brandStringTest1
注意事项
__brand?: never 是可选属性,不像必需的品牌属性那样阻止普通字符串赋值。它可以用于类型展示或交叉类型表达,但单靠它无法实现严格的名义类型隔离。如果需要强品牌约束,应使用必需的品牌属性,并在创建值时通过经过检查的构造函数或类型断言赋予该类型。
备注:内容仅供参考。
