为何TypeScript品牌类型在索引类型比较中无法区分?
问题:品牌化字符串作为索引类型时无法正确区分类型
你遇到的问题是:虽然CatID和BirdID是互相不兼容的品牌化字符串类型,但使用它们作为索引键的对象类型在extends检查中却被判定为兼容,而直接检查两个品牌类型的兼容性是正确的(返回false)。
代码复现
type Brand<K, T> = K & { __type: T }; type CatID = Brand<string,'cat'>; type BirdID = Brand<string,'bird'>; // 意外返回true const x1: {[id:CatID]:string} extends {[id:BirdID]:boolean} ? true : false; const x2: {[id in CatID]:string} extends {[id in BirdID]:boolean} ? true : false; const x3: Record<CatID,string> extends Record<BirdID,boolean> ? true : false; // 正确返回false const x4: CatID extends BirdID ? true : false;
原因分析
TypeScript对索引签名键类型的处理会忽略基于普通字符串属性的品牌标记:
- 你定义的
Brand类型是通过将原始类型与{ __type: T }相交来模拟名义类型,但__type是一个普通的字符串字面量属性。 - 当这种品牌化类型被用作索引键时,TypeScript会“剥离”品牌标记,只保留底层的原始类型(这里是
string)。 - 因此,
{[id:CatID]:string}和{[id:BirdID]:boolean}都会被视为本质上的string索引签名类型,此时TypeScript会基于值类型的逆变规则进行兼容性检查,最终返回true。
解决方案:使用唯一Symbol作为品牌标记
要让TypeScript正确区分不同品牌化类型的索引键,需要使用唯一Symbol来定义品牌,因为Symbol的唯一性会被TypeScript保留,不会被剥离:
// 创建唯一的Symbol作为品牌标识 const CatBrand = Symbol('cat'); const BirdBrand = Symbol('bird'); // 基于Symbol定义品牌化类型 type CatID = string & { [typeof CatBrand]: never }; type BirdID = string & { [typeof BirdBrand]: never }; // 现在所有extends检查都返回正确的结果 const x1: {[id:CatID]:string} extends {[id:BirdID]:boolean} ? true : false; // ^? const x1: false const x2: {[id in CatID]:string} extends {[id in BirdID]:boolean} ? true : false; // ^? const x2: false const x3: Record<CatID,string> extends Record<BirdID,boolean> ? true : false; // ^? const x3: false const x4: CatID extends BirdID ? true : false; // ^? const x4: false
这种方法通过唯一的Symbol属性确保TypeScript能识别不同品牌类型的结构差异,从而在索引签名的兼容性检查中正确区分它们。
内容的提问来源于stack exchange,提问作者Nimai
相关产品推荐
相关产品推荐

