如何在TypeScript中创建带严格约束的字面量派生类型?
TypeScript实现类型安全的数值/字符串类型隔离
需求说明
需要在TypeScript里给number和string类型加一层安全约束:
- 不能直接把字面量数值/字符串或者其他类型赋值给自定义类型变量,要触发编译错误
- 同类型的数值可以运算,结果自动推断成该类型;不同类型的数值不能赋值也不能混合运算
- 字符串类型也要有同样的约束
之前试过交叉类型方案:
type Meter = number & { __type: 'meter' }; type Second = number & { __type: 'second' };
但这个方案有俩问题:
- 不同类型混着运算不会报错
- 同类型运算的结果没法自动推断类型,必须手动用
as断言
解决方案
咱们可以用类型标记+运算符重载的类型体操来实现完整的类型安全,不用装第三方包。
1. 定义基础类型和安全断言函数
先给数值和字符串类型加唯一的编译期标记,同时写个断言函数避免随便用as导致的不安全:
// 数值类型模板:带唯一单位标记 type NumberUnit<T extends string> = number & { __unit: T }; // 字符串类型模板:带唯一标签标记 type StringTag<T extends string> = string & { __tag: T }; // 安全创建数值单位类型:只能传number,返回对应单位类型 function asUnit<T extends string>(value: number): NumberUnit<T> { return value as NumberUnit<T>; } // 安全创建字符串标签类型:只能传string,返回对应标签类型 function asTag<T extends string>(value: string): StringTag<T> { return value as StringTag<T>; } // 自定义具体类型 type Meter = NumberUnit<'meter'>; type Second = NumberUnit<'second'>; type Title = StringTag<'title'>; type Author = StringTag<'author'>;
2. 重载运算符实现类型安全运算
通过TypeScript的条件类型和全局接口重载,让同类型运算返回原类型,不同类型直接触发编译错误:
// 定义加法运算的返回类型:同类型返回原类型,不同类型返回never(触发错误) type Add<T extends NumberUnit<string>, U extends NumberUnit<string>> = T['__unit'] extends U['__unit'] ? T : never; // 定义减法运算的返回类型 type Subtract<T extends NumberUnit<string>, U extends NumberUnit<string>> = T['__unit'] extends U['__unit'] ? T : never; // 全局重载Number的运算符类型(仅编译阶段生效,不影响运行时) declare global { interface Number { // 加法:仅同类型可以运算 +<T extends NumberUnit<string>>(this: T, other: T): T; +<T extends NumberUnit<string>, U extends NumberUnit<string>>(this: T, other: U): T['__unit'] extends U['__unit'] ? T : never; // 减法:仅同类型可以运算 -<T extends NumberUnit<string>>(this: T, other: T): T; -<T extends NumberUnit<string>, U extends NumberUnit<string>>(this: T, other: U): T['__unit'] extends U['__unit'] ? T : never; } }
3. 验证效果
数值类型测试
let length1 = asUnit<'meter'>(5); let length2 = asUnit<'meter'>(7); let duration1 = asUnit<'second'>(10); let duration2 = asUnit<'second'>(11); // ✅ 同类型运算自动推断类型 let lengthSum = length1 + length2; // 类型为Meter,值12 let durationSum = duration1 + duration2; // 类型为Second,值21 // ❌ 不同类型赋值报错:类型不兼容 length1 = duration2; // ❌ 不同类型混合运算报错:返回never,触发编译错误 let foo = length1 + duration1;
字符串类型测试
let myBookTitle = asTag<'title'>("Best title ever"); let myBookAuthor = asTag<'author'>("John Doe"); // ❌ 不同类型赋值报错:类型不兼容 myBookTitle = myBookAuthor;
补充说明
- 这些
__unit/__tag标记只在编译阶段存在,运行时完全没有额外开销 - 用
asUnit/asTag代替直接as,能避免把错误类型的值强行断言成目标类型 - 运算符重载通过条件类型限制只有同类型能运算,不同类型直接返回
never,编译器会直接报错
内容的提问来源于stack exchange,提问作者Derek Svart
相关产品推荐
相关产品推荐

