You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在TypeScript中创建带严格约束的字面量派生类型?

TypeScript实现类型安全的数值/字符串类型隔离

需求说明

需要在TypeScript里给number和string类型加一层安全约束:

  • 不能直接把字面量数值/字符串或者其他类型赋值给自定义类型变量,要触发编译错误
  • 同类型的数值可以运算,结果自动推断成该类型;不同类型的数值不能赋值也不能混合运算
  • 字符串类型也要有同样的约束

之前试过交叉类型方案:

type Meter = number & { __type: 'meter' };
type Second = number & { __type: 'second' };

但这个方案有俩问题:

  1. 不同类型混着运算不会报错
  2. 同类型运算的结果没法自动推断类型,必须手动用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 02:38:20