# 冷门-字面量类型和基础类型
string vs 'a'、number vs 1、boolean vs true —— 这两类类型的关系在 TS 体操里经常埋坑。本文把字面量类型、基础类型之间的关系以及 widening / narrowing 的行为说清楚。
# 概念
字面量类型 (literal type) 是"只能是这个具体值"的类型:
type A = 'hello'; // 字符串字面量
type B = 42; // 数字字面量
type C = true; // 布尔字面量
基础类型 (primitive type) 是它们的"父类型":
type S = string;
type N = number;
type Bo = boolean;
一个字面量类型是对应基础类型的子类型:
type T1 = 'a' extends string ? true : false; // true
type T2 = 1 extends number ? true : false; // true
反过来不成立:
type T3 = string extends 'a' ? true : false; // false
这条关系在条件类型里用来区分"字面量入参"和"宽泛入参"。
# 判断是不是字面量
题目常见需求:"只有入参是字面量才处理,否则报错"。反向思路:基础类型 extends 字面量类型为 false。
type IsStringLiteral<S> = S extends string
? string extends S
? false
: true
: false;
type A = IsStringLiteral<'hello'>; // true
type B = IsStringLiteral<string>; // false
type C = IsStringLiteral<'a' | 'b'>; // true(联合的每支都是字面量)
这里利用 "字面量 extends 基础类型 ✓,基础类型 extends 字面量 ✗" 的单向关系。
对数字字面量 / 布尔字面量同理。
# Widening(宽化)
用 const 声明的字面量赋值会被推成"字面量类型";用 let 声明则会被宽化到基础类型:
const a = 'hello'; // 类型:'hello'
let b = 'hello'; // 类型:string
类型层里没有 let / const,但当字面量出现在某些上下文(比如作为函数默认值、泛型参数推断)时,TS 会默认宽化到基础类型。要想保留字面量类型,要么用 as const,要么在类型参数上加约束。
function f<T extends string>(x: T): T {
return x;
}
f('hello'); // T 被推成 'hello'
f('hello' as string); // T 被推成 string
# ${number} / ${string} 模板类型
模板字符串类型本身就暗含了宽/窄的概念:
type T1 = `${1}`; // '1',字面量被保留
type T2 = `${number}`; // `${number}`,一个"所有数字字符串"的类型
type T3 = `${string}`; // string(TS 会把它等价为 string)
题目里看到 ${number} / ${bigint} 模板,一般都是在描述"一个符合数字格式的字符串",比较时不是字面量等同,而是模式匹配:
type IsDigitString<S> = S extends `${number}` ? true : false;
type A = IsDigitString<'42'>; // true
type B = IsDigitString<'abc'>; // false
type C = IsDigitString<'1.5'>; // true,浮点也算
# 联合里的字面量 vs 基础类型
一旦联合里混入了基础类型,整个联合会"升级"到基础类型的行为:
type U = 'a' | 'b' | string;
type R = U; // string
TS 会把 'a' | 'b' | string 合并成 string,因为基础类型能覆盖字面量。写题时如果想保留字面量支,就不能混入基础类型。
类似地:
type U2 = number | 1; // number
# 特殊值:true | false
boolean 在类型层等价于 true | false:
type T1 = boolean extends true | false ? true : false; // true
type T2 = true | false extends boolean ? true : false; // true
这意味着对 boolean 做分发会得到两支:
type F<T> = T extends true ? 1 : 2;
type R = F<boolean>; // 1 | 2
体操题里经常用这个特点做"分支汇总",也要小心它带来的意外分发。
# any / unknown / never
这三个"特殊基础类型"在条件类型里各有怪癖:
type A1 = any extends string ? 1 : 2; // 1 | 2
type A2 = unknown extends string ? 1 : 2; // 2
type A3 = never extends string ? 1 : 2; // never(分发)
any extends X总是同时匹配两支,返回A | B。unknown extends X除了X = unknown / any,其它都为false。never作为裸泛型分发时直接短路成never。
这些细节在判 any / 判 never / 判 unknown 的题里经常要用。
# 字面量比较 vs 基础类型比较
比较两个类型相等时,字面量只要完全相同就算相等:
type E1 = 'a' extends 'a' ? true : false; // true
但"字面量联合"与对应基础类型不是 Equal 相等,尽管在 extends 层面是可赋值的:
type E2 = [string] extends ['a' | string] ? true : false; // true
type E3 = Equals<string, 'a' | string>; // false(内部 literal 记忆不同)
Equals 的实现机制会区分"同结构不同出处"的类型,详见 判断两个类型相等。
# 被谁用到
- 判字面量:hard/223-isAny 的对称思路、medium/16259-转换为基本类型。
- 数字字符串:medium/10969-整数, medium/1978-百分比解析器, hard/300-stringToNumber。
- boolean 分发:medium/12-可串联构造器 里的状态分叉。
- 基础类型吸收字面量:解释"为什么一混 string 就失去字面量"的常见误会,出现在很多题的边界讨论中。
记住"字面量是基础类型的子集,方向单向"这句,配合 ${number}、boolean = true | false 和 any/unknown/never 的特殊行为,大部分坑都能自己识别。
← 进阶-逆变顺变协变 算法-排列组合大乱炖 →