TypeScript 实用技巧:解决字面量类型与 string 不匹配的问题

134 阅读2分钟

在 TypeScript 日常开发中,我们常常会遇到这样一种场景:在定义参数类型的时候,我们会希望参数类型更加具体,而不是更宽泛 stringnumberboolean

比如,将 method 的类型定义为 "GET" | "POST" ,可以限制这个参数仅为固定的字符串 "GET""POST",而不是任意字符串。相比于仅将参数定义为 string , 如果传入参数不正确,TS 会在编译时就抛出错误,从而免去了添加手动校验参数的逻辑。

这里的 "GET""POST" 称为字面量类型 (Literal Type)。将多个字面量类型通过 | 连接起来就组成了联合字面量类型,这种技巧在 TS 类型定义中非常常见。

然而,这种类型定义方式会造成一些“反直觉”的问题:

// 定义函数签名,method 的类型为联合字面量类型而不是 string
declare function handleRequest(url: string, method: "GET" | "POST"): void;

// 构造请求体,此处的 method 会被 TS 推断为 string 类型
const req = { url: "https://example.com", method: "GET" };

// ❌ 报错:Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
handleRequest(req.url, req.method);

将字符串 "GET" 传递给类型限制为 "GET" | "POST" 的参数时不被 TS 允许。TS 认为种情况是一种错误,因为 req.method 被推断为 string,而不是 "GET"

string-is-not-assignable-to-type.png

解决这种宽泛的原始类型和收窄的字面量类型不匹配的方法有以下几种

方法 1 : 添加类型断言

通过 as 类型断言,让 TS 知道此处的 req.method 的具体类型。可以在构造对象或者在调用对象时断言:

// 构造时断言
const req = { url: "https://example.com", method: "GET" as "GET" };
// 调用时断言
handleRequest(req.url, req.method as "GET");

也可以显式为 req 对象声明类型:

// 显示声明类型
type GetRequest = {
  url: string;
  method: "GET";
};

const req: GetRequest = { url: "https://example.com", method: "GET" };

handleRequest(req.url, req.method);

方法 2 : as const

as const 允许 TS 保留字面量类型,同时会为每个字段的类型加上修饰符 readonly

The as const suffix acts like const but for the type system, ensuring that all properties are assigned the literal type instead of a more general version like string or number.

as const 后缀在类型系统中类似于 const ,确保所有属性都被分配为字面类型,而不是更通用的类型,如 string 或 number

const req = { url: "https://example.com", method: "GET" } as const; // ✅ 此时 req.method 的类型时 "GET" 而不是 string

此时 req.method 的类型时 "GET" 而不是 string

as-const.png

方法 3 : 抽离枚举 enum

一般采用联合字面量类型的地方都可以改用枚举类型。在类型定义时使用枚举类型,在消费该类型的地方使用枚举值,就不会有类型错误。

enum Method {
	GET = "GET",
	POST = "POST",
};

// 构造和调用使用同一套类型
declare function handleRequest(url: string, method: Method): void;

const req = { url: "https://example.com", method: Method.GET };