# 冷门-字面量类型和基础类型

string vs 'a'number vs 1boolean 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 的实现机制会区分"同结构不同出处"的类型,详见 判断两个类型相等

# 被谁用到

记住"字面量是基础类型的子集,方向单向"这句,配合 ${number}boolean = true | falseany/unknown/never 的特殊行为,大部分坑都能自己识别。

Last Updated: 2026/4/19 00:27:20