TypeScript 学习笔记

一份完整的 TypeScript 学习笔记。学多少记多少,重点讲"为什么这么设计"(费曼式 📌 讲解),覆盖从入门到工程实战的完整知识体系。

怎么用这份笔记

  1. 学习/复习 → 顺序看正文,重点看 📌 类比和"为什么"
  2. 查语法 → 翻 附录 A 命令速查
  3. 面试 → 做 附录 B 闪卡,盖住答案自答
  4. 落地 → 跟 附录 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 编译器做了两件事:

  1. 类型检查:根据类型规则检查代码是否有类型错误
  2. 类型擦除 + 降级:移除所有类型注解,根据 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 的用途:

  1. 标记永远不会返回的函数(死循环、抛异常)
  2. 穷尽检查(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 主要用于:

  1. 与旧代码兼容
  2. 全局类型声明合并(如扩展 Vue/Lodash 等库的类型)
  3. .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 的类型系统能操作字符串。这在以下场景极其有用:

  1. API 路由类型安全:type Route = "/users/:id" → 自动提取 :id
  2. 事件系统:on${EventName} → onClick、onHover 等
  3. CSS-in-JS:属性名类型检查
  4. 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 类型系统的极限挑战——递归、条件推断、模板字面量组合。日常项目中不需要写出这样的类型,但理解原理能帮你:

  1. 看懂第三方库的 .d.ts 文件
  2. 写出更精确的 API 类型
  3. 理解内置工具类型的实现原理

实践建议:掌握 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 最佳实践总结:

  1. strict: true — 一律开启
  2. 避免 any — 用 unknown + 类型守卫
  3. 精确类型 — 用字面量联合替代宽泛的 string
  4. 可辨识联合 — 替代大量可选属性
  5. 类型守卫 > 类型断言 — 安全优先
  6. interface 优先 — 对象形状用 interface
  7. import type — 纯类型导入用 import type
  8. 不写过度复杂的类型 — 可读性 > 类型精确性 :::

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 · 实战项目清单

  1. 类型安全的 API 客户端:用泛型 + 可辨识联合封装 fetch,自动推断响应类型
  2. 表单验证库:用映射类型 + 条件类型生成验证规则类型,错误消息类型安全
  3. 事件系统:用模板字面量类型 + 映射类型实现 on<EventName> 类型提示
  4. 状态机:用可辨识联合 + 条件类型实现有限状态机,非法状态转换编译报错
  5. ORM 类型推导:用 infer + 映射类型从 Schema 定义推导模型类型
  6. 路由参数解析:用模板字面量类型从 "/users/:id/posts/:postId" 提取参数类型
本页目录