TypeScript 学习笔记
一份完整的 TypeScript 学习笔记。学多少记多少,重点讲"为什么这么设计"(费曼式 📌 讲解),覆盖从入门到工程实战的完整知识体系。
怎么用这份笔记
- 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
- 查语法 → 翻 附录 A 命令速查
- 面试 → 做 附录 B 闪卡,盖住答案自答
- 落地 → 跟 附录 C 实战项目做一遍
图表在 /TypeScript学习笔记/diagrams/ 目录。图注名即原图文件名,看图注就能直接找到原图用 draw.io 编辑。
1 · TypeScript 概述与设计理念
1.1 TypeScript 是什么
- TypeScript(简称 TS)由 Microsoft 的 Anders Hejlsberg(也是 C# 和 Delphi 之父)于 2012 年开发
- 定位:JavaScript 的超集,在 JS 之上添加了静态类型系统
- 核心目标:在编译时捕获类型错误,让大型 JS 项目更可维护、更安全
- 编译产物:纯 JavaScript,TS 本身不运行在任何运行时上
TIP
📌 打个比方:JavaScript 像在黑暗中开车——灵活但容易撞墙(运行时才发现错误)。TypeScript 像装了车灯和导航——编译时就能看到前方的弯路,提前规避。车灯本身不改变车(编译后还是 JS),但让你开得更安全。
📌 TS vs JS 的核心区别:
- JS:动态类型,错误在运行时暴露
- TS:静态类型,错误在编译时暴露
- TS 编译为 JS 后零运行时开销——类型信息全部被擦除
:::
1.2 为什么不是纯 JavaScript
| 问题 |
JavaScript |
TypeScript |
| 类型检查 |
运行时报错 |
编译时报错 |
| 重构 |
全局搜索,容易遗漏 |
改类型定义,编译器帮你找 |
| IDE 补全 |
基于推断,不完整 |
类型驱动,精确补全 |
| 大型项目协作 |
难以维护 |
接口即文档 |
| 团队沟通 |
靠注释和文档 |
类型定义就是契约 |
:::tip
📌 TS 的设计哲学:"渐进式类型增强"。你可以从纯 JS 逐步迁移——先给函数加返回类型,再给参数加类型,最后给整个项目加类型。TS 兼容所有合法 JS 代码,allowJs: true 甚至允许 JS 文件参与编译。这是 TS 能成功取代 Flow/CoffeeScript 的关键——零迁移成本起步。
1.3 TypeScript 的编译流程
.ts 文件 → TypeScript 编译器 (tsc) → .js 文件 + .d.ts 声明文件
↓
Node.js / 浏览器运行
TIP
📌 TS 编译器做了两件事:
- 类型检查:根据类型规则检查代码是否有类型错误
- 类型擦除 + 降级:移除所有类型注解,根据
target 降级为指定版本的 JS
类型信息在编译后完全消失——运行时没有任何类型信息。这意味着你不能在运行时用 typeof 检查 TS 类型(typeof 在运行时还是 JS 的 typeof)。
2 · 开发环境搭建
2.1 安装
# 全局安装 TypeScript 编译器
npm install -g typescript
# 验证
tsc --version
# 项目内安装(推荐)
npm install -D typescript
npx tsc --version
2.2 初始化项目
# 生成 tsconfig.json
npx tsc --init
# 运行 TS 文件(无需手动编译)
npm install -D ts-node
npx ts-node hello.ts
2.3 tsconfig.json 核心配置
{
"compilerOptions": {
"target": "ES2020", // 编译目标 JS 版本
"module": "ESNext", // 模块系统
"moduleResolution": "node", // 模块解析策略
"strict": true, // 开启所有严格类型检查
"esModuleInterop": true, // 兼容 CommonJS import
"skipLibCheck": true, // 跳过 .d.ts 检查(加速编译)
"outDir": "./dist", // 输出目录
"rootDir": "./src", // 源码目录
"declaration": true, // 生成 .d.ts 声明文件
"sourceMap": true // 生成 source map
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
TIP
📌 strict: true 做了什么?一次性开启所有严格模式:
strictNullChecks:null/undefined 不能赋给其他类型
noImplicitAny:禁止隐式 any
strictFunctionTypes:函数参数双向检查改为逆变
strictBindCallApply:bind/call/apply 严格类型检查
alwaysStrict:输出 "use strict"
noImplicitThis:禁止 this 为隐式 any
建议:新项目一律 strict: true,这是 TS 的最佳实践。
3 · 基础类型
3.1 原始类型
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let n: null = null;
let u: undefined = undefined;
let big: bigint = 100n;
let sym: symbol = Symbol("id");
3.2 数组与元组
// 数组
let arr: number[] = [1, 2, 3];
let arr2: Array<string> = ["a", "b"];
// 元组(固定长度和类型的数组)
let tuple: [string, number] = ["hello", 42];
let tuple2: [string, number, boolean] = ["a", 1, true];
// 可选元素
let tuple3: [string, number?] = ["a"];
TIP
📌 元组 vs 数组:数组是"同类型不定长",元组是"特定类型定长"。元组本质上是数组,但 TS 在编译时检查每个位置的类型。元组常用于:函数多返回值、CSV 行解析、Promise.all 结果。
3.3 enum 枚举
// 数字枚举(默认从 0 开始)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up; // 0
// 字符串枚举
enum Color { Red = "RED", Green = "GREEN", Blue = "BLUE" }
let c: Color = Color.Red; // "RED"
// 异构枚举(不推荐)
enum Mixed { No = 0, Yes = "YES" }
// const enum(内联,编译后消失)
const enum Status { Active, Inactive }
let s = Status.Active; // 编译为: let s = 0;
TIP
📌 const enum vs enum:普通 enum 编译后生成一个对象(运行时存在)。const enum 编译时内联——所有引用直接替换为值,不生成任何运行时代码。如果你只需要枚举值不需要运行时对象,用 const enum 更高效。
📌 枚举的陷阱:数字枚举可以反向映射(Direction[0] → "Up"),字符串枚举不行。数字枚举的成员类型是 number,不是字面量——这导致类型不够精确。替代方案:用联合字面量类型 type Direction = "up" | "down" | "left" | "right"。
3.4 any、unknown、never、void
// any:放弃类型检查(尽量少用)
let a: any = 42;
a = "hello"; // OK
a.foo(); // OK(编译器不检查)
// unknown:类型安全的 any
let u: unknown = 42;
u.toFixed(); // 错误!必须先类型收窄
if (typeof u === "number") {
u.toFixed(); // OK
}
// never:永远不会有值的类型
function infiniteLoop(): never {
while (true) {}
}
function throwError(msg: string): never {
throw new Error(msg);
}
// void:函数没有返回值
function log(msg: string): void {
console.log(msg);
}
TIP
📌 any vs unknown——TS 类型安全的分水岭:
any:我不管类型了,编译器别检查——等于关闭了 TS
unknown:我不知道类型,但你必须先检查才能用——类型安全的 any
实践建议:当你确实不知道类型时,永远用 unknown 而非 any。unknown 强制你做类型收窄,保留了类型安全。
📌 never 的用途:
- 标记永远不会返回的函数(死循环、抛异常)
- 穷尽检查(exhaustive check):在 switch 中确保所有枚举值都被处理
type Shape = "circle" | "square";
function area(s: Shape) {
switch (s) {
case "circle": return Math.PI;
case "square": return 1;
default:
const _exhaustive: never = s; // 如果漏了 case,这里会报错
return _exhaustive;
}
}
3.5 类型断言
// 尖括号语法(JSX 中不可用)
let len: number = (<string>someValue).length;
// as 语法(推荐,JSX 兼容)
let len2: number = (someValue as string).length;
// 双重断言(当类型不重叠时)
let s = (obj as unknown) as string;
TIP
📌 类型断言 ≠ 类型转换。断言不改变运行时值,只告诉编译器"我知道这个值是什么类型"。它不安全——如果断言错误,运行时会出错。尽量少用,优先用类型守卫。
📌 as const:让 TS 推断最窄的字面量类型:
const obj = { name: "Alice", age: 25 } as const;
// 类型: { readonly name: "Alice"; readonly age: 25 }
4 · 接口与类型别名
4.1 接口(interface)
interface User {
name: string;
age: number;
email?: string; // 可选属性
readonly id: number; // 只读属性
}
const u: User = { name: "Alice", age: 25, id: 1 };
// 函数接口
interface SearchFn {
(source: string, keyword: string): boolean;
}
const search: SearchFn = (s, k) => s.includes(k);
// 可索引接口
interface StringArray {
[index: number]: string;
}
const arr: StringArray = ["a", "b"];
4.2 接口继承
interface Animal {
name: string;
}
interface Dog extends Animal {
bark(): void;
}
interface Cat extends Animal {
meow(): void;
}
// 多继承
interface Bird extends Animal, Dog {
fly(): void;
}
4.3 类型别名(type)
type UserName = string;
type ID = number | string;
type Callback = (data: any) => void;
type Point = {
x: number;
y: number;
};
type Tree = {
value: number;
left?: Tree;
right?: Tree;
};
4.4 interface vs type
// interface 可以重复声明(自动合并)
interface Window { foo: string; }
interface Window { bar: number; }
// 合并为: { foo: string; bar: number }
// type 不能重复声明
type Foo = string;
// type Foo = number; // 错误
TIP
📌 interface vs type 怎么选?
| 能力 |
interface |
type |
| 对象类型 |
是 |
是 |
| 联合类型 |
否 |
是 A | B |
| 交叉类型 |
否 |
是 A & B |
| 元组 |
否 |
是 |
| 条件类型 |
否 |
是 |
| 映射类型 |
否 |
是 |
| 声明合并 |
是 |
否 |
| 继承 |
extends |
& 交叉 |
实践建议:对象形状用 interface(可扩展、可合并),联合/交叉/条件/映射等高级类型用 type。社区共识是优先 interface,需要 type 的能力时再用 type。
5 · 函数
5.1 函数类型
// 完整函数类型
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数
const multiply = (a: number, b: number): number => a * b;
// 类型别名
type MathFn = (a: number, b: number) => number;
const divide: MathFn = (a, b) => a / b; // 参数类型自动推断
5.2 可选参数与默认值
// 可选参数(必须在必选参数后面)
function greet(name: string, greeting?: string): string {
return `${greeting || "Hello"}, ${name}`;
}
// 默认值(自动推断为可选)
function greet2(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}`;
}
// 剩余参数
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
5.3 函数重载
// 重载签名
function padLeft(value: string, padding: number): string;
function padLeft(value: string, padding: string): string;
// 实现签名(对外不可见)
function padLeft(value: string, padding: number | string): string {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
}
return padding + value;
}
padLeft("hello", 3); // " hello"
padLeft("hello", ">>"); // ">>hello"
padLeft("hello", true); // 错误
TIP
📌 为什么需要函数重载?TS 的函数重载不是真正的多态(不像 Java/C++),而是为同一个实现提供多个类型签名。调用方看到的是重载签名,实现签名对外不可见。这让你能精确描述不同参数组合的返回类型。
📌 重载的陷阱:实现签名必须兼容所有重载签名。TS 只用重载签名做类型检查,实现签名只用于内部实现。常见错误是忘了写实现签名。
6 · 类
6.1 类基础
class Animal {
// 属性修饰符
public name: string; // 公开(默认)
private id: number; // 私有(类内可访问)
protected species: string; // 受保护(类和子类可访问)
readonly birth: Date; // 只读
// 静态属性
static count: number = 0;
// 构造函数
constructor(name: string, species: string) {
this.name = name;
this.species = species;
this.id = ++Animal.count;
this.birth = new Date();
}
// 方法
public describe(): string {
return `${this.name} is a ${this.species}`;
}
}
6.2 构造函数简写
// 参数属性:构造函数参数直接声明为类属性
class User {
constructor(
public name: string, // 自动创建 public name 属性
private age: number, // 自动创建 private age 属性
readonly email: string // 自动创建 readonly email 属性
) {}
}
const u = new User("Alice", 25, "a@b.com");
console.log(u.name); // "Alice"
TIP
📌 参数属性是 TS 独有的语法糖。constructor(public name: string) 等价于:
class User {
public name: string;
constructor(name: string) {
this.name = name;
}
}
一行代替三行,非常实用。
6.3 继承与抽象类
// 抽象类
abstract class Shape {
abstract area(): number; // 抽象方法,子类必须实现
protected color: string;
constructor(color: string) {
this.color = color;
}
}
class Circle extends Shape {
constructor(color: string, private radius: number) {
super(color);
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
class Square extends Shape {
constructor(color: string, private side: number) {
super(color);
}
area(): number {
return this.side ** 2;
}
}
6.4 implements vs extends
// interface 定义能力
interface Comparable {
compareTo(other: this): number;
}
// implements:实现接口(只检查形状,不继承实现)
class Money implements Comparable {
constructor(public amount: number) {}
compareTo(other: Money): number {
return this.amount - other.amount;
}
}
// extends:继承父类(获得实现)
class Dollar extends Money {
constructor(amount: number) {
super(amount);
}
}
TIP
📌 implements vs extends:
extends:继承——子类获得父类的所有实现,单继承
implements:实现——类承诺符合接口的形状,不获得任何实现,可多实现
一个类可以 extends 一个父类 + implements 多个接口:
class Dog extends Animal implements Comparable, Serializable {}
6.5 访问器 getter/setter
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) throw new Error("Below absolute zero");
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * 9/5 + 32;
}
set fahrenheit(value: number) {
this._celsius = (value - 32) * 5/9;
}
}
7 · 泛型
7.1 泛型函数
// 泛型函数:T 是类型参数
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // 显式指定 T = string
identity(42); // 推断 T = number
// 多类型参数
function pair<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
pair("name", "Alice"); // [string, string]
pair(1, true); // [number, boolean]
7.2 泛型接口与类
// 泛型接口
interface Box<T> {
value: T;
}
const strBox: Box<string> = { value: "hello" };
const numBox: Box<number> = { value: 42 };
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
get size(): number { return this.items.length; }
}
const s = new Stack<number>();
s.push(1);
s.push(2);
s.pop(); // 2
7.3 泛型约束
// 约束 T 必须有 length 属性
function getLength<T extends { length: number }>(arg: T): number {
return arg.length;
}
getLength("hello"); // 5
getLength([1, 2, 3]); // 3
// getLength(42); // number 没有 length
// 使用 keyof 约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
getProperty({ name: "Alice", age: 25 }, "name"); // string
getProperty({ name: "Alice", age: 25 }, "age"); // number
// getProperty({ name: "Alice" }, "email"); // key 不存在
TIP
📌 泛型约束 extends:限制类型参数的范围。T extends U 表示 T 必须可赋值给 U。这是泛型类型安全的核心——不是所有 T 都能用,只有满足约束的 T 才行。
📌 keyof T 是 TS 泛型的灵魂之一。它获取 T 的所有键的联合类型,让你能安全地访问对象属性。上面的 getProperty 是最经典的泛型模式——类型安全的属性访问。
7.4 默认泛型类型
interface Container<T = string> {
value: T;
}
const c1: Container = { value: "hello" }; // T 默认为 string
const c2: Container<number> = { value: 42 }; // 显式指定 number
8 · 联合类型与交叉类型
8.1 联合类型(Union)
// 一个值可以是多种类型之一
type ID = number | string;
function findUser(id: ID) {
if (typeof id === "string") {
id.toUpperCase(); // TS 知道这里是 string
} else {
id.toFixed(); // TS 知道这里是 number
}
}
// 字面量联合
type Status = "pending" | "success" | "error";
type Direction = "up" | "down" | "left" | "right";
// 对象联合(可辨识联合)
interface Success { status: "success"; data: string; }
interface Error { status: "error"; message: string; }
type Result = Success | Error;
function handle(r: Result) {
if (r.status === "success") {
console.log(r.data); // TS 知道是 Success
} else {
console.log(r.message); // TS 知道是 Error
}
}
TIP
📌 可辨识联合(Discriminated Union) 是 TS 中最强大的模式之一。多个类型共享一个公共属性(如 status),通过检查该属性来收窄类型。这比 typeof 更强大——可以区分不同的对象类型。
📌 联合类型的 | 读作"或"。number | string 意味着"这个值是 number 或 string"。使用前必须用类型守卫收窄到具体类型。
8.2 交叉类型(Intersection)
// 将多个类型合并为一个
type Named = { name: string };
type Aged = { age: number };
type Person = Named & Aged;
const p: Person = { name: "Alice", age: 25 }; // 必须同时满足两个类型
// 实用场景:混入(mixin)
type WithTimestamp = { createdAt: Date; updatedAt: Date };
type AuditableUser = User & WithTimestamp;
TIP
📌 交叉类型的 & 读作"且"。A & B 意味着"这个值必须同时满足 A 且 B"。相当于把两个类型的属性合并在一起。
📌 联合 vs 交叉:
- 联合
A | B:或——满足 A 或 B 即可(属性减少)
- 交叉
A & B:且——必须同时满足 A 和 B(属性增加)
记忆法:联合是"取并集",交叉是"取交集"——但注意"取交集"指的是属性的并集(都要有),不是类型的交集。
9 · 类型守卫
9.1 typeof 守卫
function process(value: string | number) {
if (typeof value === "string") {
value.toUpperCase(); // string
} else {
value.toFixed(); // number
}
}
9.2 instanceof 守卫
class Cat { meow(): void {} }
class Dog { bark(): void {} }
function speak(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow();
} else {
animal.bark();
}
}
9.3 in 守卫
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim();
} else {
animal.fly();
}
}
9.4 自定义类型谓词
// is 关键字:自定义类型守卫
function isString(value: unknown): value is string {
return typeof value === "string";
}
function process(value: unknown) {
if (isString(value)) {
value.toUpperCase(); // TS 知道 value 是 string
}
}
// 复杂例子:检查对象是否包含特定属性
function hasProperty<K extends string>(
obj: unknown,
key: K
): obj is Record<K, unknown> {
return typeof obj === "object" && obj !== null && key in obj;
}
TIP
📌 类型谓词 value is T 是 TS 最优雅的特性之一。你写一个函数返回 boolean,但用 is 告诉编译器"如果返回 true,那么参数就是 T 类型"。这让类型收窄可以自定义——不限于 typeof/instanceof/in。
📌 类型守卫的本质:TS 编译器在 if 分支中收窄类型。这不是运行时行为——运行时 if 还是普通 if。TS 只是在编译时分析控制流,推断每个分支中变量的类型。
10 · 类型推断
10.1 基本推断
let x = 42; // 推断为 number
let s = "hello"; // 推断为 string
let arr = [1, 2, 3]; // 推断为 number[]
let obj = { name: "Alice", age: 25 }; // { name: string; age: number }
// 函数返回值推断
function add(a: number, b: number) {
return a + b; // 返回类型推断为 number
}
10.2 上下文推断
// 根据上下文推断参数类型
window.onmousedown = (mouseEvent) => {
// TS 推断 mouseEvent: MouseEvent
console.log(mouseEvent.button);
};
// 根据返回类型推断
const names = ["Alice", "Bob", "Charlie"];
names.forEach((name) => {
// TS 推断 name: string
console.log(name.toUpperCase());
});
10.3 最佳通用类型
// 混合类型数组推断为联合类型
let arr = [1, "hello", true];
// 推断为 (string | number | boolean)[]
// 但 const 推断为字面量类型
const x = 42; // 42(字面量)
const s = "hello"; // "hello"(字面量)
const arr = [1, 2, 3] as const; // readonly [1, 2, 3]
TIP
📌 let vs const 的类型推断差异:
let x = 42 → 推断为 number(变量可重新赋值,类型放宽)
const x = 42 → 推断为 42(字面量类型,不可重新赋值)
📌 as const 的威力:让 TS 推断最窄的字面量类型。const obj = { a: 1 } 推断为 { a: number },而 const obj = { a: 1 } as const 推断为 { readonly a: 1 }。这在需要精确字面量类型的场景(如配置对象、状态机)非常有用。
11 · 模块系统
11.1 导入导出
// export 命名导出
export function add(a: number, b: number): number { return a + b; }
export const PI = 3.14159;
// export 默认导出(每个模块最多一个)
export default class Calculator {}
// 导入
import Calculator, { add, PI } from "./calculator";
// 重命名导入
import { add as plus } from "./calculator";
// 导入类型(编译后擦除)
import type { User } from "./types";
// 动态导入
const module = await import("./heavy-module");
11.2 类型导出
// types.ts
export interface User {
name: string;
age: number;
}
export type ID = number | string;
// 使用
import type { User, ID } from "./types";
// 或
import { type User, type ID } from "./types";
TIP
📌 import type 的意义:明确告诉编译器"我只导入类型,不导入值"。编译后这行会被完全擦除——不会出现在 JS 输出中。这避免了循环依赖问题和不必要的运行时导入。
📌 内联 type 修饰符:import { type User, getValue } from "./types" 可以混合导入类型和值,type 标记的会被擦除。
11.3 命名空间(namespace)
namespace Utils {
export function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
export const VERSION = "1.0";
}
Utils.clamp(5, 0, 10); // 5
TIP
📌 namespace vs module:现代 TS 项目几乎不用 namespace。ES Module(import/export)已经完全取代了 namespace 的作用。namespace 主要用于:
- 与旧代码兼容
- 全局类型声明合并(如扩展 Vue/Lodash 等库的类型)
- .d.ts 声明文件中组织类型
新项目一律用 ES Module,不要用 namespace。
12 · 装饰器
12.1 类装饰器
// 类装饰器:接收构造函数,返回新的构造函数
function Logged<T extends new (...args: any[]) => any>(constructor: T) {
return class extends constructor {
created = new Date().toISOString();
};
}
@Logged
class Service {
constructor(public name: string) {}
}
const s = new Service("api");
console.log((s as any).created); // 时间戳
12.2 方法装饰器
// 方法装饰器:(target, propertyKey, descriptor)
function Log(target: any, key: string, desc: PropertyDescriptor) {
const original = desc.value;
desc.value = function (...args: any[]) {
console.log(`Calling ${key} with`, args);
return original.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
12.3 属性装饰器与参数装饰器
// 属性装饰器
function Required(target: any, key: string) {
// 标记属性为必填(配合验证框架)
}
// 参数装饰器
function Inject(target: any, key: string, index: number) {
// 依赖注入标记
}
class UserService {
@Required name!: string;
save(@Inject repo: Repository) {}
}
TIP
📌 装饰器是实验性特性:需要 experimentalDecorators: true 在 tsconfig 中开启。装饰器广泛用于 NestJS、TypeORM 等后端框架。TC39 装饰器提案仍在推进中,TS 5.0+ 支持标准装饰器语法。
📌 装饰器执行顺序:实例方法 → 静态方法 → 属性 → 类。多个装饰器从下到上、从内到外执行。
13 · 声明文件
13.1 .d.ts 文件
// types.d.ts — 只包含类型,不包含实现
declare function greet(name: string): void;
declare const VERSION: string;
declare namespace MyLib {
function init(config: Config): void;
interface Config { debug: boolean; }
}
13.2 为 JS 库写声明
// my-lib.d.ts
declare module "my-lib" {
export function add(a: number, b: number): number;
export function subtract(a: number, b: number): number;
export const PI: number;
}
// 使用
import { add, PI } from "my-lib"; // 有类型提示了
13.3 三斜线指令
/// <reference types="node" />
/// <reference path="./custom-types.d.ts" />
// 现代项目中,推荐用 tsconfig 的 types 和 include 代替
TIP
📌 声明文件的作用:为纯 JS 库提供类型信息。@types/xxx 包就是社区维护的声明文件(如 @types/node、@types/lodash)。如果库自带 TS 类型(types 字段指向 .d.ts),则不需要 @types 包。
📌 declare 关键字:告诉 TS "这个变量/函数/模块已经存在了,我只是告诉你它的类型"。declare 不生成任何 JS 代码——纯类型声明。
14 · tsconfig 配置详解
14.1 核心配置项
{
"compilerOptions": {
/* 基本配置 */
"target": "ES2020", // 编译目标
"module": "ESNext", // 模块系统
"moduleResolution": "bundler", // 模块解析策略(TS 5.0+)
"lib": ["ES2020", "DOM"], // 可用的类型库
/* 严格模式 */
"strict": true, // 总开关
"noUnusedLocals": true, // 未使用的局部变量报错
"noUnusedParameters": true, // 未使用的参数报错
"noImplicitReturns": true, // 函数路径必须都有返回值
"noFallthroughCasesInSwitch": true, // switch case 必须有 break
/* 模块解析 */
"esModuleInterop": true, // CJS/ESM 互操作
"allowSyntheticDefaultImports": true, // 允许合成默认导入
"resolveJsonModule": true, // 允许 import JSON
"skipLibCheck": true, // 跳过 .d.ts 检查
/* 输出 */
"outDir": "./dist",
"rootDir": "./src",
"declaration": true, // 生成 .d.ts
"declarationMap": true, // 生成 .d.ts.map
"sourceMap": true, // 生成 .js.map
"removeComments": true, // 移除注释
/* 路径别名 */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}
}
TIP
📌 lib 配置很重要:它决定 TS 知道哪些全局 API。"lib": ["ES2020", "DOM"] 意味着你可以用 ES2020 的 Promise.allSettled 和 DOM 的 document.querySelector。Node.js 项目用 "lib": ["ES2020"](不加 DOM),前端项目加 "DOM"。
📌 路径别名 paths:让 @/components/Button 映射到 src/components/Button。需要同时配置 baseUrl。注意:tsc 编译后路径不会自动替换,需要 tsc-alias 或 bundler 处理。
15 · 工具类型(Utility Types)
15.1 常用内置工具类型
interface User {
id: number;
name: string;
age: number;
email: string;
}
// Partial<T>:所有属性变可选
type PartialUser = Partial<User>;
// { id?: number; name?: string; age?: number; email?: string; }
// Required<T>:所有属性变必选
type RequiredUser = Required<PartialUser>;
// Readonly<T>:所有属性变只读
type ReadonlyUser = Readonly<User>;
// { readonly id: number; readonly name: string; ... }
// Pick<T, K>:选取部分属性
type UserName = Pick<User, "name" | "email">;
// { name: string; email: string; }
// Omit<T, K>:排除部分属性
type UserWithoutId = Omit<User, "id">;
// { name: string; age: number; email: string; }
// Record<K, V>:构造键值对类型
type UserMap = Record<string, User>;
// ReturnType<T>:获取函数返回类型
function getUser() { return { name: "Alice", age: 25 }; }
type User2 = ReturnType<typeof getUser>;
// Parameters<T>:获取函数参数类型(元组)
type Params = Parameters<typeof getUser>;
// Awaited<T>:解包 Promise<T>
type Data = Awaited<Promise<string>>; // string
15.2 工具类型速查表
| 工具类型 |
作用 |
示例 |
Partial<T> |
全部可选 |
Partial<User> |
Required<T> |
全部必选 |
Required<PartialUser> |
Readonly<T> |
全部只读 |
Readonly<User> |
Pick<T, K> |
选取属性 |
Pick<User, "name"> |
Omit<T, K> |
排除属性 |
Omit<User, "id"> |
Record<K, V> |
键值对 |
Record<string, User> |
ReturnType<T> |
函数返回类型 |
ReturnType<typeof fn> |
Parameters<T> |
函数参数类型 |
Parameters<typeof fn> |
Awaited<T> |
解包 Promise |
Awaited<Promise<T>> |
NonNullable<T> |
排除 null/undefined |
NonNullable<T | null> |
keyof T |
获取所有键 |
keyof User |
typeof V |
获取值的类型 |
typeof obj |
InstanceType<T> |
构造函数实例类型 |
InstanceType<typeof Class> |
TIP
📌 工具类型是 TS 的"标准库"。它们是泛型类型别名,用类型级别的编程来实现类型变换。掌握这些工具类型是 TS 进阶的关键——你不需要自己写复杂的类型逻辑,大部分场景内置工具类型就够了。
16 · 条件类型
16.1 基本语法
// T extends U ? X : Y
// 如果 T 可赋值给 U,则类型为 X,否则为 Y
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes"(字面量也是 string)
16.2 条件类型分配
// 当 T 是联合类型时,条件类型会分配(distribute)
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>;
// 分配为: ToArray<string> | ToArray<number>
// 结果: string[] | number[]
// 阻止分配:用 [T] 包裹
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type R2 = ToArrayNonDist<string | number>;
// 结果: (string | number)[]
TIP
📌 分配条件类型是 TS 类型系统最微妙的行为之一。当 T 是裸类型参数(naked type parameter)且是联合类型时,条件类型会对联合的每个成员分别计算,然后合并结果。用 [T] extends [U] 可以阻止分配。
这个特性在写高级工具类型时非常重要——你需要知道什么时候分配、什么时候不分配。
16.3 infer 关键字
// infer:在条件类型中推断类型变量
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
type R1 = UnpackPromise<Promise<string>>; // string
type R2 = UnpackPromise<number>; // number
// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type R3 = ElementOf<string[]>; // string
// 提取函数第一个参数
type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never;
type R4 = FirstParam<(a: number, b: string) => void>; // number
TIP
📌 infer 是 TS 类型体操的核心。它让你在条件类型中"捕获"一个未知类型,类似正则表达式的捕获组。infer U 意味着"如果 T 匹配这个模式,把 U 绑定到匹配的部分"。
常见模式:
Promise<infer U> → 提取 Promise 的值类型
(infer E)[] → 提取数组元素类型
(...args: infer P) => any → 提取函数参数
Return<infer R> → 提取返回类型
:::
17 · 映射类型
17.1 基本映射
// 遍历类型的所有键,对每个键的值类型做变换
type Stringify<T> = {
[K in keyof T]: string;
};
interface User { name: string; age: number; }
type StringUser = Stringify<User>;
// { name: string; age: string; }
// 实际上 Partial<T> 的实现就是映射类型
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
17.2 映射修饰符
// +? / -? 控制可选
type Required2<T> = {
[K in keyof T]-?: T[K]; // 移除可选
};
// +readonly / -readonly 控制只读
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // 移除只读
};
17.3 键重映射(Key Remapping,TS 4.1+)
// as 子句:重映射键名
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { name: string; age: number; }
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number; }
// 过滤键
type StringKeys<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
type R = StringKeys<User>; // { name: string; }(age 被过滤掉)
:::tip
📌 映射类型 = 类型级别的 map。就像 JS 的 Object.keys(obj).map(key => transform(key, obj[key])),映射类型在类型级别遍历对象的所有键,对每个键做变换。这是 TS 类型编程最强大的工具之一。
18 · 模板字面量类型
18.1 基本语法
// 字符串级别的类型操作
type Greeting = `hello ${string}`;
const g: Greeting = "hello world"; // OK
const g2: Greeting = "hi world"; // 错误
// 联合类型展开
type Direction = "up" | "down";
type Command = `move ${Direction}`;
// "move up" | "move down"
18.2 实用模式
// 事件名类型
type EventName = `on${Capitalize<string>}`;
type Handler = (e: Event) => void;
type Events = Record<EventName, Handler>;
// CSS 属性
type CSSProperty = `${string}-${string}`;
// 路由参数
type Route = `/users/${number}/posts/${number}`;
const r: Route = "/users/1/posts/42"; // OK
18.3 内置字符串工具类型
type U = Uppercase<"hello">; // "HELLO"
type L = Lowercase<"HELLO">; // "hello"
type C = Capitalize<"hello">; // "Hello"
type U2 = Uncapitalize<"Hello">; // "hello"
TIP
📌 模板字面量类型让 TS 的类型系统能操作字符串。这在以下场景极其有用:
- API 路由类型安全:
type Route = "/users/:id" → 自动提取 :id
- 事件系统:
on${EventName} → onClick、onHover 等
- CSS-in-JS:属性名类型检查
- getter/setter 生成:
get${CapitalizedKey}
这是 TS 4.1+ 的杀手级特性——在类型级别做字符串拼接和变换。
19 · 类型体操
19.1 经典类型体操题
// 1. DeepPartial:递归可选
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
// 2. DeepReadonly:递归只读
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
// 3. PickByValueType:按值类型选取属性
type PickByValueType<T, V> = {
[K in keyof T as T[K] extends V ? K : never]: T[K];
};
type StringProps = PickByValueType<User, string>;
// 4. Get<Path>:从嵌套对象类型中按路径获取类型
type Get<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T ? Get<T[Key], Rest> : never
: P extends keyof T ? T[P] : never;
type Obj = { user: { name: string; age: number } };
type NameType = Get<Obj, "user.name">; // string
// 5. UnionToTuple(高级):联合类型转元组
// 这是一个非常复杂的类型体操,实际项目中很少用到
19.2 实用自定义工具类型
// 获取函数的 this 类型
type ThisParameter<T> = T extends (this: infer This, ...args: any[]) => any ? This : unknown;
// 不可选属性
type NonOptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T];
// 可选属性
type OptionalKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? K : never
}[keyof T];
// 两个类型的差异属性
type Diff<T, U> = Omit<T, keyof U> & Omit<U, keyof T>;
TIP
📌 类型体操要不要学?类型体操是 TS 类型系统的极限挑战——递归、条件推断、模板字面量组合。日常项目中不需要写出这样的类型,但理解原理能帮你:
- 看懂第三方库的
.d.ts 文件
- 写出更精确的 API 类型
- 理解内置工具类型的实现原理
实践建议:掌握 infer、条件类型、映射类型这三个核心,能解决 95% 的类型需求。剩下 5% 的极限场景可以查类型体操仓库。
20 · 项目最佳实践
20.1 项目结构
my-project/
├── src/
│ ├── types/ # 全局类型定义
│ │ ├── index.d.ts
│ │ └── env.d.ts
│ ├── utils/ # 工具函数
│ ├── components/ # 组件(前端)
│ ├── services/ # 服务层
│ ├── models/ # 数据模型
│ └── index.ts
├── tsconfig.json
├── package.json
└── README.md
20.2 类型设计原则
// 好:精确的类型
interface User {
id: string; // 不是 number | string
name: string;
role: "admin" | "user"; // 不是 string
status: "active" | "inactive";
}
// 坏:过于宽泛
interface User {
id: any;
name: string;
role: string;
[key: string]: any; // 索引签名 = 放弃类型检查
}
// 好:可辨识联合
type ApiResponse =
| { status: "success"; data: User }
| { status: "error"; code: number; message: string };
// 坏:可选属性满天飞
interface ApiResponse {
status: string;
data?: User;
code?: number;
message?: string;
}
20.3 严格模式最佳实践
// 1. 开启 strict
// tsconfig.json: "strict": true
// 2. 避免 any,用 unknown 代替
function parse(json: string): unknown {
return JSON.parse(json);
}
// 3. 使用类型守卫收窄
if (typeof value === "string") { ... }
// 4. 枚举用联合字面量替代
type Status = "pending" | "success" | "error";
// 而非
// enum Status { Pending, Success, Error }
// 5. 接口优先于 type(对象类型)
interface User { ... }
// 需要联合/交叉时才用 type
// 6. 不滥用类型断言
// (value as string).toUpperCase()
// if (typeof value === "string") value.toUpperCase()
:::tip
📌 TS 最佳实践总结:
strict: true — 一律开启
- 避免
any — 用 unknown + 类型守卫
- 精确类型 — 用字面量联合替代宽泛的
string
- 可辨识联合 — 替代大量可选属性
- 类型守卫 > 类型断言 — 安全优先
interface 优先 — 对象形状用 interface
import type — 纯类型导入用 import type
- 不写过度复杂的类型 — 可读性 > 类型精确性
:::
20.4 与框架集成
// Vue 3 + TS
import { defineComponent, ref, computed } from "vue";
export default defineComponent({
props: {
msg: { type: String, required: true }
},
setup(props) {
const count = ref(0); // Ref<number>
const doubled = computed(() => count.value * 2);
return { count, doubled };
}
});
// React + TS
import { useState } from "react";
function Counter() {
const [count, setCount] = useState<number>(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// NestJS + TS(装饰器 + DI)
@Controller("users")
export class UserController {
constructor(private readonly userService: UserService) {}
@Get(":id")
findOne(@Param("id") id: string): Promise<User> {
return this.userService.findOne(id);
}
}
附录 A · TypeScript 命令速查
| 命令 |
说明 |
tsc --init |
生成 tsconfig.json |
tsc |
编译项目 |
tsc --watch |
监听模式自动编译 |
tsc --noEmit |
只做类型检查不输出 |
tsc --strict |
临时开启严格模式 |
tsc --target ES2020 |
指定编译目标 |
tsc file.ts |
编译单个文件 |
ts-node file.ts |
直接运行 TS 文件 |
ts-node --transpile-only |
跳过类型检查只转译(更快) |
tsc --showConfig |
打印最终合并的配置 |
tsc --traceResolution |
调试模块解析 |
附录 B · 面试闪卡
Q: TS 和 JS 的关系?
A: TS 是 JS 的超集,添加了静态类型系统。TS 编译为 JS 后类型信息全部擦除,运行时零开销。所有合法 JS 都是合法 TS。
Q: any 和 unknown 的区别?
A: any 放弃类型检查,可以随便操作。unknown 是类型安全的 any——必须先做类型收窄(typeof/instanceof/in/类型谓词)才能操作。永远优先 unknown。
Q: interface 和 type 的区别?
A: interface 支持声明合并、继承(extends),适合对象类型。type 支持联合/交叉/条件/映射/元组等高级类型。对象形状优先 interface,需要高级类型用 type。
Q: never 类型有什么用?
A: 1) 标记永不返回的函数(死循环/抛异常)。2) 穷尽检查——在 switch 的 default 分支赋值给 never 类型变量,如果漏了 case 会编译报错。
Q: 什么是可辨识联合?
A: 多个类型共享一个公共属性(如 status/type),通过检查该属性来收窄类型。比 typeof/instanceof 更强大,能区分不同对象类型。是 TS 中最实用的模式之一。
Q: infer 关键字的作用?
A: 在条件类型中捕获未知类型。T extends Promise<infer U> ? U : T 提取 Promise 的值类型。类似正则的捕获组——"如果匹配这个模式,把某部分绑定到变量"。
Q: 条件类型的分配律是什么?
A: 当 T 是裸类型参数且是联合类型时,T extends U ? X : Y 会对联合的每个成员分别计算再合并。用 [T] extends [U] 可以阻止分配。
Q: as const 做了什么?
A: 让 TS 推断最窄的字面量类型,并将所有属性变为 readonly。{ a: 1 } as const → { readonly a: 1 }。常用于配置对象和状态机。
Q: keyof T 是什么?
A: 获取 T 的所有键的联合类型。keyof { name: string; age: number } → "name" | "age"。是泛型约束和映射类型的基础。
Q: Partial<T> 是怎么实现的?
A: 映射类型:{ [K in keyof T]?: T[K] }。遍历 T 的所有键,给每个键加 ? 变为可选。
附录 C · 实战项目清单
- 类型安全的 API 客户端:用泛型 + 可辨识联合封装 fetch,自动推断响应类型
- 表单验证库:用映射类型 + 条件类型生成验证规则类型,错误消息类型安全
- 事件系统:用模板字面量类型 + 映射类型实现
on<EventName> 类型提示
- 状态机:用可辨识联合 + 条件类型实现有限状态机,非法状态转换编译报错
- ORM 类型推导:用
infer + 映射类型从 Schema 定义推导模型类型
- 路由参数解析:用模板字面量类型从
"/users/:id/posts/:postId" 提取参数类型