基础类型注解

给变量、数组、对象标注类型,让编译器帮你把关

🎯 学习目标

学完本章,你将能够:

  • 能够为变量、常量添加 number、string、boolean 等基础类型注解
  • 能够理解并运用 unknown、any、never、void 等特殊类型
  • 能够掌握类型推断规则,在合适时机选择显式标注
  • 能够使用类型断言(as、<>)在确定场景下收窄类型
  • 能够理解元组、字面量类型与 const 断言的使用场景

怎么告诉 TS

这个变量是数字,那个是字符串,别搞混了

TypeScript 的类型注解语法很简单:在变量名后面加 : 类型。

// JS 写法:没有类型
let name = "张三";

// TS 写法:加上类型注解
let name: string = "张三";

这一章我们来学习所有基础类型的注解方式。

Part 1: 原始类型

string、number、boolean——和 JS 完全对应,只是多了类型注解

TypeScript 的类型注解语法很简单:在变量名后面加 : 类型。这一节从最基础的三个类型开始——string、number、boolean,它们和 JS 完全对应,只是多了一层编译时检查。同时你还会学到什么时候该手动标注,什么时候让 TS 自己推断。

三大原始类型

字符串、数字、布尔值的类型注解

// 字符串
let name: string = "张三";
name = "李四";        // OK
// name = 42;          // 报错:不能将 number 赋值给 string

// 数字
let age: number = 25;
let hex: number = 0xff;    // 十六进制
let binary: number = 0b1010; // 二进制
let float: number = 3.14;

// 布尔
let isActive: boolean = true;
let hasPermission: boolean = false;

用人话说:类型注解就像给变量贴标签——"这个盒子只能装字符串"。你往里塞数字,编译器就会提醒你贴错了标签。

更多原始类型

bigint、symbol 以及类型推断

// bigint(ES2020)
let big: bigint = 9007199254740993n;

// symbol
let sym: symbol = Symbol("unique");

// 类型注解的两种方式
let x: number = 42;      // 显式注解
let y = 42;               // 类型推断,等同于 let y: number = 42
类型推断:TS 很聪明,很多时候不需要手动标注。变量初始化时赋的值是什么类型,TS 就自动推断变量是什么类型。

Part 2: 数组类型

数组的元素类型也可以标注,防止混入错误类型的数据

原始类型的注解掌握了,但实际开发中你用得最多的可能是数组。JS 数组什么都能往里塞,数字、字符串混着来也不报错。TypeScript 可以给数组标注元素类型,确保 number[] 里不会冒出字符串。这一节还会学到只读数组、联合类型数组,以及 as const 怎么把数组"冻住"。

数组注解方式

T[] 和 Array<T> 两种写法等价

// JS 写法:没有类型约束
let nums = [1, 2, 3];
nums.push("hello"); // 不会报错

// TS 写法1:T[](推荐)
let nums: number[] = [1, 2, 3];
// nums.push("hello"); // 报错!只能 push 数字

// TS 写法2:Array
let names: Array<string> = ["张三", "李四"];
// 多维数组
let matrix: number[][] = [[1, 2], [3, 4]];

// 只读数组
let readonlyArr: readonly number[] = [1, 2, 3];
// readonlyArr.push(4);  // 报错
// readonlyArr[0] = 99;  // 报错

联合与不可变

数组元素可以是多种类型,也可以锁定为不可变

// 联合类型数组:元素可以是 string 或 number
let mixed: (string | number)[] = [1, "hello", 2, "world"];

// 使用 const 断言创建不可变数组
const COLORS = ["red", "green", "blue"] as const;
// 类型为 readonly ["red", "green", "blue"]
// COLORS.push("yellow"); // 报错
// COLORS[0] = "pink";   // 报错

Part 3: 空值处理

null 和 undefined 是最常见的错误来源,TypeScript 有专门的机制来处理它们

Cannot read property of null——你一定见过这个报错。null 和 undefined 是 JS 里最狡猾的"地雷",踩到就炸。TypeScript 开启 strictNullChecks 后,null 不能随便赋值给其他类型,编译器逼你先检查再使用。这一节学会用可选链 ?. 和空值合并 ?? 安全地处理空值。

严格空值检查

开启 strict 后,null 不能随便赋值

// strictNullChecks 开启后

// JS 写法:不会报错,运行时可能崩溃
// let name = null;
// name.toUpperCase(); // Cannot read property of null

// TS 写法:编译时就拦截
// let name: string = null;        // 报错!
// let age: number = undefined;    // 报错!

// 正确做法:使用联合类型
let name: string | null = null;
let age: number | undefined = undefined;
// 可选属性自动添加 | undefined
interface User {
    name: string;
    email?: string;  // 等同于 email: string | undefined
}

// 可选参数也一样
function greet(name?: string) {
    // name 的类型是 string | undefined
    console.log(name?.toUpperCase());
}

非空断言

告诉 TS "我确定这个值不是 null"

// 非空断言操作符 !
let input = document.getElementById("app")!;
// 告诉 TS:我确定这个元素存在,别报错

// 可选链与空值合并配合使用
let user: { name: string | null } = { name: null };
let displayName = user.name ?? "匿名";  // "匿名"

建议:始终开启 strictNullChecks。虽然需要多写一些 | null,但能避免大量运行时的 Cannot read property of null 错误。

☕ 学习断点

你已经完成了前半部分,休息一下吧

前半部分我们学习了原始类型、数组类型、空值处理和字面量类型——这些都是 TS 类型系统的基础。

建议:休息 5 分钟,回顾一下学习目标,确认都达成了再继续。

💡 后半部分将学习 any/unknown、类型推断、never 和类型断言——这些是写出类型安全代码的关键。

Part 4: 字面量类型

变量只能取特定的值——比枚举更轻量

有些变量不该接受任意字符串,比如 HTTP 方法只能是 "GET" 或 "POST"。字面量类型就是干这个的:把变量的取值范围限定在你指定的几个值里。比枚举更轻量,写法也更简单。这一节看完你就知道怎么用字面量类型做"穷举式"的参数校验。

限定取值范围

变量只能是你指定的那几个值

// 字符串字面量
let direction: "left" | "right";
direction = "left";     // OK
// direction = "up";    // 报错

// 数字字面量
let statusCode: 200 | 404 | 500;
statusCode = 200;       // OK
// statusCode = 201;    // 报错

// 实际应用:限定参数值
type Method = "GET" | "POST" | "PUT" | "DELETE";
function request(url: string, method: Method) {
    // method 只能是以上四个值之一
}
request("/api/users", "GET");     // OK
// request("/api/users", "FETCH"); // 报错
// 结合 as const 实现字面量推断
const config = {
    env: "development" as const,  // 类型为 "development" 而非 string
    port: 3000 as const,          // 类型为 3000 而非 number
};

Part 5: any 与 unknown

当你不知道类型时,有两种选择——一种安全,一种不安全

不是所有值的类型都能提前确定。第三方 API 返回的数据、动态解析的 JSON,类型都是"未知"的。这时候有两个选择:any 关闭检查,unknown 要求先检查再使用。这一节对比两者的区别,告诉你为什么 any 是"万恶之源",以及 unknown 怎么安全替代它。

any:关闭检查

万恶之源,能不用就不用

// any —— 关闭类型检查
let anything: any = "hello";
anything = 42;          // OK
anything = {};          // OK
anything.foo();         // 编译不报错,运行时崩溃
anything.bar.baz;       // 编译不报错,运行时崩溃
any 本质上就是回到了 JS。它关闭了所有类型检查,等于白用 TypeScript。只在迁移旧代码时临时使用。

unknown:安全替代

类型安全的 any,使用前必须先检查

// unknown —— 类型安全的 any
let unsure: unknown = "hello";
unsure = 42;            // OK——可以赋任何值

// 但使用前必须收窄(narrow)
// unsure.toUpperCase();  // 报错!不能直接使用

// 正确用法1:typeof 检查
if (typeof unsure === "string") {
    console.log(unsure.toUpperCase()); // OK
}

// 正确用法2:类型断言
console.log((unsure as string).toUpperCase());

// 正确用法3:类型守卫函数
function isString(val: unknown): val is string {
    return typeof val === "string";
}

三者对比

any vs unknown vs never

类型含义安全性使用场景
any任何值,跳过检查无迁移旧代码、快速原型
unknown任何值,使用前须收窄高第三方 API 响应、泛型
never不可能存在的值-穷尽检查、不可能的分支

Part 6: 类型推断

很多时候不需要手动标注,TS 会自动推断类型

写了这么多类型注解,你可能会问:每个变量都要手动标注吗?不用。TypeScript 很聪明,你写 let x = 42,它就知道 x 是 number。但推断不是万能的——函数参数就不会推断,对象和数组有时推断得不够精确。这一节搞清楚什么时候该让 TS 推断,什么时候必须手动标注。

推断规则

能推断就让 TS 推断,减少冗余代码

// 变量初始化自动推断
let x = 42;         // 推断为 number
let s = "hello";    // 推断为 string
let b = true;       // 推断为 boolean
let arr = [1, 2];   // 推断为 number[]

// 对象推断
let user = {
    name: "张三",   // string
    age: 25,        // number
};
// user 的类型: { name: string; age: number }
// 函数返回值推断
function add(a: number, b: number) {
    return a + b;  // 自动推断返回 number
}

// 函数参数不会自动推断(需要手动标注)
// function greet(name) { } // 报错:参数隐式有 any 类型
function greet(name: string) { } // 正确

何时手动标注

四种必须手动标注类型的情况

函数参数

参数类型不会自动推断,必须手动标注。

更宽泛的类型

let arr: (string|number)[] = [] 需要显式声明。

返回值不明确

复杂函数的返回值类型最好标注清楚。

const 断言

as const 让推断更精确,锁定字面量类型。

原则:能推断就让 TS 推断。函数参数必须标注,返回值通常可以推断。

元组类型

长度固定、每个位置类型也固定的数组

普通数组所有元素类型相同,但有时你需要"第0个是字符串,第1个是数字"这种精确控制。元组就是干这个的。

// 普通数组:所有元素类型相同
let names: string[] = ["张三", "李四"];

// 元组:每个位置的类型固定
let userTuple: [string, number] = ["张三", 25];
//                       类型↑   类型↑
// userTuple[0] 是 string,userTuple[1] 是 number

// 越界或类型不对都会报错
// userTuple = [25, "张三"];  // 报错!顺序反了
// userTuple = ["张三"];      // 报错!缺少第二个元素
// 实际应用场景:函数返回多个值
function useCoords(): [number, number] {
    return [39.9, 116.4]; // 经纬度
}
const [lat, lng] = useCoords();
// lat 和 lng 自动推断为 number

// RGB 颜色值
let color: [number, number, number] = [255, 128, 0];

元组在 React 的 useState 返回值中就用到了:[state, setState] 就是一个元组。虽然你可能没注意到,但你已经在用元组了。

类型拓宽

为什么 let 和 const 推断的结果不一样?

看看这个"诡异"的现象:

// let 会"拓宽"类型
let age = 25;        // 推断为 number(不是 25)
age = 30;            // OK——因为类型是 number

// const 会"收窄"为字面量
const AGE = 25;      // 推断为 25(字面量类型)
// AGE = 30;         // 报错!25 不能赋值为 30
// 对象和数组的拓宽更隐蔽
let obj = { x: 1 };     // { x: number }
const obj2 = { x: 1 };  // 还是 { x: number }!
// const 只锁定了变量本身,对象内部还是会拓宽

// 解决方案:as const
const obj3 = { x: 1 } as const;  // { readonly x: 1 }
// obj3.x = 2;  // 报错!

let arr = [1, 2, 3];    // number[]
const arr2 = [1, 2, 3] as const; // readonly [1, 2, 3]
记住:let 拓宽为宽泛类型,const 收窄为字面量。但 const 对象和数组的内部属性仍然会被拓宽,需要 as const 才能锁定。
图:类型拓宽的三种路径对比

类型层次

从 any 到 never,TS 类型的完整谱系

图:TypeScript 类型系统层次结构
// 类型谱系:从"什么都能装"到"什么都不能装"
// any    → 关闭检查,什么都能装(危险)
// unknown → 什么都能装,但用之前必须检查(安全)
// string / number / boolean / object → 具体类型
// "hello" / 42 / true → 字面量类型(最精确)
// never  → 不可能的值,什么都不能装
类型能赋值给它能赋值给别人一句话总结
any任何类型任何类型逃出类型系统的后门
unknown任何类型收窄后才行安全的"不确定"
stringstringstring 超集具体类型
"hello"仅 "hello"string字面量,最精确
nevernever(不可能)任何类型不可能存在的值

Part 7: never 详解

never 表示永远不可能存在的值

前面的对比表中提到过 never,但没细说。它是 TS 类型系统中最"极端"的类型——表示永远不可能存在的值。抛异常的函数返回 never,不可能的交叉类型结果是 never,switch 的 default 分支也能用 never 做穷尽检查。这一节深入理解 never 的三大出现场景和它的实战用途。

never 是什么

不可能存在的值的类型

never 不会出现在你的变量声明里,但会在以下场景自动出现:

// 场景 1:不可能的交叉类型
type Impossible = string & number;
// 类型是 never——一个值不可能同时是 string 和 number

// 场景 2:永远不会返回的函数
function fail(msg: string): never {
    throw new Error(msg); // 抛出异常,函数不会正常返回
}

function infiniteLoop(): never {
    while (true) {} // 死循环,永远不会返回
}

// 场景 3:收窄到不可能的分支
function check(x: string | number) {
    if (typeof x === "string") {
        // x 是 string
    } else if (typeof x === "number") {
        // x 是 number
    } else {
        // x 的类型是 never——这个分支永远走不到
        console.log(x); // never
    }
}

可以这么理解:never 是类型系统的"底线"——如果一个变量被推断为 never,说明逻辑上这个分支不可能到达,代码有问题或者只是防御性写法。

never 详解(上)

never 出现的三大场景与 void 区别

// 场景 1:不可能的联合交叉
type StrAndNum = string & number;   // never
type Both = "left" & "right";       // never

// 场景 2:类型收窄后的死代码分支
function assert(x: string | number) {
    if (typeof x === "string") {
        x;  // string
    } else if (typeof x === "number") {
        x;  // number
    } else {
        x;  // never —— 这个分支永远不会执行
    }
}

// 场景 3:never 在联合类型中会被自动过滤
type Mixed = string | never | number;
// 等价于 string | number,never 被吞掉了!
type Filtered = Extract;
// Extract 排除后,剩下的还是 number
never vs void 一句话区分:void 表示"我选择不返回值"(函数正常结束),never 表示"这个函数根本回不来"(抛异常或死循环)。void 是合法的返回类型,never 是"不可能到达终点"的类型。

never 穷尽检查

让 switch 语句永远不会漏掉分支

// 穷尽检查(Exhaustive Check)的经典写法
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "square"; size: number };

function area(shape: Shape): number {
    switch (shape.kind) {
        case "circle": return Math.PI * shape.radius ** 2;
        case "square": return shape.size ** 2;
        default:
            // 如果 Shape 新增了类型,这里会报错!
            const _exhaustive: never = shape;
            throw new Error(`未知图形: ${_exhaustive}`);
    }
}
// 更通用的 assertNever 工具函数
function assertNever(x: never): never {
    throw new Error("不可能到达的分支: " + x);
}

// 使用方式:
type Status = "loading" | "success" | "error";

function handleStatus(status: Status) {
    switch (status) {
        case "loading": return "加载中...";
        case "success": return "成功";
        case "error": return "出错了";
        default: return assertNever(status);
        // 如果 Status 新增了值,这里会报编译错误
    }
}
好处:当你给 Shape 或 Status 新增一个变体,却忘了在 switch 里处理,TS 会在 default 分支报错。这就是 never 的威力——让遗漏的分支无所遁形。
图:never 穷尽检查的工作原理

never 详解(下)

assertNever 模式与穷尽 switch 实战

// 经典 exhaustive switch 模式
type Shape =
    | { kind: "circle"; radius: number }
    | { kind: "square"; size: number };

function area(shape: Shape): number {
    switch (shape.kind) {
        case "circle": return Math.PI * shape.radius ** 2;
        case "square": return shape.size ** 2;
        default:
            const _exhaustive: never = shape;
            throw new Error(`Unknown: ${_exhaustive}`);
    }
}
// 如果新增 triangle 但忘加 case:
// type Shape = ... | { kind: "triangle"; base: number };
// default 分支的 _exhaustive 就不再是 never
// TS 会报错:不能将 Shape 赋值给 never → 你立刻发现漏了!

// 实际项目中的类型保护
type ApiResponse =
    | { status: "success"; data: string }
    | { status: "error"; message: string }
    | { status: "loading" };

function handle(res: ApiResponse) {
    switch (res.status) {
        case "success": return res.data;
        case "error":   throw new Error(res.message);
        case "loading": return "请稍候...";
        default:        return assertNever(res);
    }
}

穷尽检查的核心逻辑:在 default 分支把值赋给 never 类型的变量。如果所有分支都处理了,值在 default 里一定是 never,赋值合法;如果漏了分支,值的类型就不是 never,编译立即报错。

never 对比 void

返回值类型中两个容易混淆的类型

类型含义函数能否正常返回使用场景
void没有有意义的返回值能,返回 undefined不关心返回值的函数
never永远不会返回不能,要么抛异常要么死循环抛错函数、穷尽检查
// void:函数正常执行完毕,只是没有返回值
function log(msg: string): void {
    console.log(msg);
    // 函数到这里正常结束,隐式返回 undefined
}
const result = log("hello"); // result 是 void(实际值是 undefined)

// never:函数根本不会正常结束
function throwError(msg: string): never {
    throw new Error(msg);
    // 后面的代码永远执行不到
}

// 区别在于:void 是"我选择不返回",never 是"我根本回不来"

简单记:void = 函数能跑完但没返回值;never = 函数跑不完(抛错或死循环)。

Part 8: 类型断言

有时候你比 TS 更了解一个值的类型,类型断言就是告诉编译器"相信我"

document.getElementById 返回 HTMLElement | null,但你确定页面上有这个元素。每次都要判空太烦了,能不能直接告诉 TS"它不是 null"?类型断言就是干这个的——as HTMLInputElement 一写,TS 就信你了。但滥用断言等于给自己挖坑,这一节学会正确使用断言和 as const。

断言语法与场景

两种写法、三种常见场景

// 语法 1:as Type(推荐,TSX 中也能用)
let val: unknown = "hello";
let len: number = (val as string).length;

// 语法 2:(JSX/TSX 中不能用)
let len2: number = (<string>val).length;
// 场景 1:DOM API(最常见)
const input = document.getElementById("email") as HTMLInputElement;
// 没有 as,TS 认为它可能是 null,无法访问 .value
console.log(input.value);

// 场景 2:收窄联合类型
function process(value: string | number) {
    const str = value as string; // 你确定这里是 string
    console.log(str.toUpperCase());
}

// 场景 3:从 any 迁移
interface LegacyData {
    name: string;
}
const raw: any = { name: "张三" };
const data = raw as LegacyData; // 逐步引入类型
类型断言不是类型转换! 它只在编译时告诉 TS "把值当作这个类型看待",运行时什么也不做,不会做任何转换。如果你断言错了,运行时照样报错。

断言陷阱与 const

不要滥用断言,善用 as const

// ❌ 错误用法:强制不兼容的类型
// let x: string = "hello";
// let n: number = x as number;  // 报错!string 不能断言为 number

// 双重断言:终极逃生舱(极度危险)
let n = x as unknown as number; // 先转为 unknown,再转为 number
// 能编译通过,但运行时 n 还是字符串——隐患极大!
// as const:让类型锁定为字面量
const req = { method: "GET", url: "/api/users" } as const;
// 类型变为 { readonly method: "GET"; readonly url: "/api/users" }
// 而不是 { method: string; url: string }

// 实际用处:函数参数需要字面量类型
function request(url: string, method: "GET" | "POST") {}
const options = { url: "/api", method: "GET" } as const;
request(options.url, options.method); // OK!
// 没有 as const,options.method 是 string,调用会报错

断言 vs 类型守卫:能用 if (typeof x === "string") 收窄就不用断言。类型守卫是安全的(运行时真检查了),断言只是你的一厢情愿。只有当你确实比 TS 更了解类型时才用断言。

断言深入解析

as const 全场景与双重断言的危险

// as const 三种用法
// 1. 整个对象/数组 as const
const config = { host: "localhost", port: 3000 } as const;
// { readonly host: "localhost"; readonly port: 3000 }

// 2. 单个属性 as const
const mixed = { name: "app", version: 1 as const };
// { name: string; version: 1 } — 只有 version 被锁定

// 3. 非 const 上下文中的变量
let x = "hello" as const; // x 的类型是 "hello" 而非 string
// 双重断言(as unknown as X)——万不得已才用
interface ApiRes { code: number; data: unknown; }
const raw = '{"code":200,"data":{"name":"张三"}}';
const res = JSON.parse(raw) as unknown as ApiRes;
// JSON.parse 返回 any,先用 as unknown 再转具体类型
// 实际上应该用类型守卫或校验库(如 zod)代替

// 尖括号语法在 TSX 中不能用
// value   ← TSX 里被当成 JSX 标签
// value as string ← 用这种写法就对了
黄金法则:如果可以用类型守卫(typeof、instanceof)或类型注解替代,就不要用断言。双重断言 as unknown as X 几乎总是代码异味,意味着类型设计有问题。

void 返回类型

void、undefined 与 never 在函数中的区别

// void:函数不关心返回值
function log(msg: string): void {
    console.log(msg);
}

// undefined:函数必须显式返回 undefined
function getNothing(): undefined {
    console.log("nothing");
    return undefined; // 必须写 return
}

// never:函数永远回不来
function crash(): never {
    throw new Error("boom");
}
// void 在回调中的特殊行为
type Callback = (data: string) => void;

// 回调函数返回了值也没关系!
const cb: Callback = (data) => {
    console.log(data);
    return 42; // 不报错!void 回调忽略返回值
};

// 这就是为什么 Array.forEach 的回调可以 return
[1, 2, 3].forEach((n) => {
    return n * 2; // forEach 期望 () => void,忽略返回值
});

当函数类型声明返回 void 时,实际传入的函数可以返回任意值——这个返回值会被安全忽略。这是 TS 故意设计的,让回调函数写法更灵活。

void 深入理解

void 回调设计原理与 undefined 的区别

// void 回调的设计意图
type Fn = () => string;
type VoidFn = () => void;

const f1: Fn = () => "hello";       // 返回值必须是 string
const f2: VoidFn = () => "hello";   // 返回值可以是任意类型!
// VoidFn 的实现返回什么都可以,调用方拿到的返回值始终是 void

// Array.prototype.forEach 的签名
// forEach(callback: (element: T) => void): void
// 所以回调里 return 任何值都不报错,但 forEach 会忽略它
// void vs undefined 关键区别
function returnsUndefined(): undefined {
    return undefined;  // 必须 return,否则报错
}
function returnsVoid(): void {
    // 不需要 return,隐式返回 undefined
}

// 实际应用:React 事件处理
const onClick: () => void = () => {
    return false; // TS 不报错,void 允许返回任意值
};
// 如果类型是 () => undefined,就必须 return undefined
关键规则:当函数签名声明 void 返回时,传入的实现函数可以返回任何值,但调用方拿到的返回值类型是 void(不能使用)。这是 TS 有意为之的"宽松实现、严格使用"设计。

拓宽与收窄进阶

理解 TS 的类型拓宽机制与应对策略

// 拓宽陷阱 1:数组字面量
const roles = ["admin", "user"];
// 推断为 string[],不是 ("admin" | "user")[]

// 修复:as const
const roles2 = ["admin", "user"] as const;
// 类型为 readonly ["admin", "user"]

// 拓宽陷阱 2:对象属性
const config = { env: "production" };
// config.env 的类型是 string,不是 "production"
function deploy(env: "production" | "staging") {}
// deploy(config.env); // 报错!string 不能赋给字面量联合
// 修复方案汇总
// 方案 1:as const 锁定整个对象
const config1 = { env: "production" } as const;

// 方案 2:as const 只锁定某个属性
const config2 = { env: "production" as const };

// 方案 3:显式类型注解
const config3: { env: "production" | "staging" } = { env: "production" };

// 对象字面量新鲜度(Freshness)
function setUser(user: { name: string }) {}
setUser({ name: "张三" }); // OK
// setUser({ name: "张三", age: 25 }); // 报错!多余属性
const obj = { name: "张三", age: 25 };
setUser(obj); // OK!变量赋值不做多余属性检查
对象字面量有新鲜度检查:直接传对象字面量时不允许多余属性,但传变量时不会检查。这是为了防止拼写错误,比如 { nmae: "张三" } 会被立即捕获。

拓宽陷阱实战

实际项目中容易踩的类型拓宽坑

// 陷阱 1:条件判断中的拓宽
const result = Math.random() > 0.5 ? "yes" : "no";
// result 类型是 string,不是 "yes" | "no"
// 修复:
const result2 = Math.random() > 0.5 ? "yes" as const : "no" as const;
// 或者:const result = ... as const

// 陷阱 2:函数返回值拓宽
function getStatus() {
    return "loading"; // 推断为 string
}
type S = ReturnType; // string,不是 "loading"
// 修复:显式标注返回类型
function getStatus2(): "loading" | "done" { return "loading"; }
// 陷阱 3:解构时丢失 as const
const point = { x: 1, y: 2 } as const;
const { x, y } = point;
// x 的类型是 1,y 的类型是 2 —— 解构保留字面量 ✓

// 但 let 解构会拓宽!
let { x: lx, y: ly } = point;
// lx 类型是 1,ly 类型是 2(const 推断不变)
// 但如果是 let 声明的变量,赋值后可以改变

// 陷阱 4:null/undefined 也参与拓宽
let val = null;   // 推断为 any(严格模式下)
let val2: string | null = null; // 正确做法
防坑口诀:需要字面量类型时,as const 最可靠;函数返回值尽量显式标注;let 声明的变量会被拓宽为宽泛类型。

const 类型参数

TS 5.0+ 用 <const T> 自动保留字面量

以前要保留字面量类型必须写 as const,TS 5.0 引入了 <const T> 让泛型函数自动保留字面量。

// 没有 const 类型参数
function getRoles(roles: T[]): T[] {
    return roles;
}
const r1 = getRoles(["admin", "user"]);
// r1 的类型是 string[],字面量信息丢失了
// 使用 const 类型参数
function getRoles(roles: T[]): T[] {
    return roles;
}
const r2 = getRoles(["admin", "user"]);
// r2 的类型是 ("admin" | "user")[],字面量保留!

// 实际应用:创建路由表
function createRoutes>(routes: T): T {
    return routes;
}
const routes = createRoutes({
    home: "/",
    about: "/about",
    contact: "/contact",
});
// routes.home 的类型是 "/" 而非 string

<const T> 的效果等同于调用方写了 as const,但好处是把决定权放在了函数定义端,调用方不用操心。适合工具函数、配置构建器等场景。

const T 对比

有无 <const T> 的行为差异

写法推断结果字面量是否保留
<T>string / number否,拓宽为宽泛类型
<const T>"admin" / 42是,保留字面量
<T>as const"admin" / 42是,但调用方需手动加
// 场景:类型安全的事件监听器
function on(
    event: T,
    handler: (payload: EventMap[T]) => void,
) {
    // event 类型保留为字面量
    // handler 的参数类型根据 event 精确定位
}
on("click", (e) => { /* e: ClickEvent */ });
on("keyup", (e) => { /* e: KeyboardEvent */ });
// on("tap", ...);  // 报错!EventMap 中没有 "tap"

<const T> 让函数定义者决定是否保留字面量,而不是依赖调用方记住写 as const。配合 extends 约束,可以实现非常精确的类型推导。

综合练习

定义一个含有多种类型的用户配置对象

任务:根据下面的需求,定义完整的类型并创建对象。

需求:一个用户配置对象,包含:用户名(字符串)、年龄(数字)、是否激活(布尔)、标签列表(字符串数组)、主题颜色(只能是 "light" 或 "dark")、可选的邮箱(字符串或空)、坐标位置(经纬度元组)。
// 你的代码:定义 UserConfig 类型,然后创建一个对象

参考答案

type Theme = "light" | "dark";
type Coords = [number, number]; // [经度, 纬度]

interface UserConfig {
    username: string;
    age: number;
    isActive: boolean;
    tags: string[];
    theme: Theme;
    email: string | null;     // 可选的,可能为空
    coords: Coords;           // 元组:经纬度
}

const config: UserConfig = {
    username: "张三",
    age: 25,
    isActive: true,
    tags: ["开发者", "前端"],
    theme: "dark",
    email: null,
    coords: [116.4, 39.9],
};

// 试试这些操作,看看 TS 的反应:
// config.theme = "blue";  // 报错!只能是 light 或 dark
// config.coords = [116.4]; // 报错!元组长度不对
// config.email = 123;     // 报错!不是 string 或 null

常见类型错误

红色波浪线是朋友,不是敌人。 它们在编译时就帮你找到了 bug,比运行时崩溃好得多。

问题一:隐式 any 报错

原因:函数参数没有标注类型,TS 不知道它是什么。

修复:给参数加上类型注解,例如 name: string。

问题二:不能将 null 赋值给 string

原因:strictNullChecks 开启后,null 不能赋给其他类型。

修复:使用联合类型 string | null。

问题三:类型 "string | number" 上不存在属性 "toFixed"

原因:联合类型只能使用所有成员共有的属性。

修复:先用 typeof 收窄类型,再调用专属方法。

试一试

练习一

任务:给下面的变量添加类型注解。

// 给每个变量加上合适的类型注解
let title = "TypeScript入门";
let pages = 320;
let published = true;
let tags = ["编程", "前端"];

参考答案

let title: string = "TypeScript入门";
let pages: number = 320;
let published: boolean = true;
let tags: string[] = ["编程", "前端"];

练习二

任务:定义一个函数,接收价格和折扣率,返回打折后的价格。加上类型注解。

// 你的代码:

参考答案

function discount(price: number, rate: number): number {
    return price * (1 - rate);
}
discount(100, 0.2); // 80

综合挑战

综合运用 never、断言、const 类型参数

需求:实现一个类型安全的事件分发器——定义事件类型映射,编写 dispatch 函数,要求穷尽所有事件类型,并使用 as const 或 <const T> 保留字面量类型。
// 你的代码:完成类型定义和函数实现

参考答案

// 步骤 1:用 as const 定义事件映射表
const EVENTS = {
    click:    { target: "button", x: 0, y: 0 },
    keydown:  { key: "Enter", code: "Enter" },
    scroll:   { direction: "down", distance: 100 },
} as const;

// 步骤 2:提取事件名和载荷类型
type EventName = keyof typeof EVENTS;
type EventPayload = typeof EVENTS[E];

// 步骤 3:分发函数——穷尽检查
function assertNever(x: never): never {
    throw new Error("未处理的事件: " + x);
}

function dispatch(
    event: E,
    payload: EventPayload,
): void {
    switch (event) {
        case "click":
            console.log(`点击位置: (${payload.x}, ${payload.y})`);
            break;
        case "keydown":
            console.log(`按键: ${payload.key}`);
            break;
        case "scroll":
            console.log(`滚动: ${payload.direction} ${payload.distance}px`);
            break;
        default:
            assertNever(event); // 新增事件时会报错
    }
}

// 步骤 4:类型安全调用
dispatch("click",   { target: "div", x: 100, y: 200 }); // OK
dispatch("keydown", { key: "Escape", code: "Escape" });  // OK
// dispatch("click", { key: "a" }); // 报错!载荷不匹配
// dispatch("hover", {});           // 报错!不存在的事件

这个练习串联了本章的核心知识点:as const 保留字面量、keyof 提取联合类型、泛型约束、never 穷尽检查、void 返回类型。能写出来,说明本章内容你已经吃透了。

📝 要点总结

本章核心知识点

原始类型

number、string、boolean、bigint、symbol、null、undefined

特殊类型

unknown 安全顶层、any 关闭检查、never 穷尽、void 无返回

类型推断

TS 自动推断,函数返回值、上下文类型可省略标注

类型断言

as 语法、非空断言 !、const 断言 as const

元组与字面量

固定长度元组、字面量类型限定取值范围

下一章

掌握了基础类型,下一章学习函数类型全解——参数、返回值、重载、类型谓词。

1 / X