POST · 前端 · 文章
TypeScript 从入门到进阶
2026/11/13 · by ZHANG XING YUN · 阅读 1 次 · 3987 字 · 预计阅读 10 分钟
本文覆盖 TypeScript 的类型系统、语法、编译配置与工程实践,可作为学习与查漏的参考。
- 适用读者:会 JavaScript、想系统掌握类型系统的开发者
- 环境:Node.js 20+、TypeScript 5.x
- 阅读方式:前六节是基础,第七到第十节是类型进阶,第十一、十二节是工程配置
一、TypeScript 是什么
TypeScript 是 JavaScript 的超集:在 JS 语法之上增加了静态类型,编译后产物仍是 JS。
需要先明确三件事:
- 类型只在编译期存在。运行时没有任何类型检查,
as断言也不会做校验。 - 类型不改变运行时行为。
interface、type、泛型在编译后全部消失。 - 类型的作用是提前暴露错误,不是"自动保证正确"。类型写错(例如滥用
any)等于放弃保护。
// 编译期报错,运行前就能发现
function add(a: number, b: number): number {
return a + b;
}
// add(1, "2"); // 编译错误:Argument of type 'string' is not assignable to parameter of type 'number'
二、安装与运行
npm init -y
npm i -D typescript
npx tsc --init # 生成 tsconfig.json
npx tsc # 按 tsconfig 编译
npx tsc --noEmit # 只做类型检查,不产出文件(CI 常用)
直接运行 TS(开发期):
npm i -D ts-node
npx ts-node src/index.ts
# Node 20.6+ 也可以用类型擦除模式(Node 22+ 原生支持 --experimental-strip-types)
node --experimental-strip-types src/index.ts
最小可用 tsconfig.json:
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"outDir": "dist",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src"]
}
strict: true 会同时开启一批检查(strictNullChecks、noImplicitAny、strictFunctionTypes 等)。新项目应当开启。
三、基础类型
3.1 原始类型
let name: string = "张三";
let age: number = 18;
let active: boolean = true;
let big: bigint = 10n;
let id: symbol = Symbol("id");
let nothing: null = null;
let missing: undefined = undefined;
3.2 数组与元组
const nums: number[] = [1, 2, 3];
const nums2: Array<number> = [1, 2, 3];
// 元组:长度与每项类型固定
const pair: [string, number] = ["a", 1];
const named: [name: string, age: number] = ["a", 1]; // 带标签,仅提示作用
// 可选元素与剩余元素
const tuple: [string, number?, ...boolean[]] = ["a"];
3.3 对象类型
let user: { id: number; name: string; email?: string } = {
id: 1,
name: "张三"
};
属性名后的 ? 表示可选,等价于 string | undefined;读取时需要用可选链或判空。
3.4 any、unknown、never、void
| 类型 | 含义 | 使用建议 |
|---|---|---|
any |
关闭类型检查 | 尽量不用,会传染 |
unknown |
未知类型,使用前必须收窄 | 接收外部数据(JSON、catch)时的正确选择 |
never |
不可能出现的值 | 穷尽检查、抛错函数的返回类型 |
void |
无返回值 | 函数返回类型 |
function parse(input: unknown): string {
if (typeof input === "string") return input;
if (typeof input === "number") return String(input);
return "";
}
function fail(message: string): never {
throw new Error(message);
}
// 穷尽检查:新增成员时这里会编译报错
type Status = "idle" | "loading" | "done";
function label(status: Status): string {
switch (status) {
case "idle":
return "空闲";
case "loading":
return "加载中";
case "done":
return "完成";
default: {
const exhaustive: never = status;
return exhaustive;
}
}
}
3.5 类型断言与类型守卫
const el = document.querySelector("#app") as HTMLDivElement;
// 非空断言:确定不是 null 时使用,滥用会掩盖问题
const node = document.querySelector("#app")!;
// 双重断言:类型确实不兼容时才用
const value = ("hello" as unknown) as number;
断言不做运行时校验。来自接口的数据应先校验(如 zod、class-validator),而不是直接 as SomeType。
四、对象类型:interface 与 type
interface User {
id: number;
name: string;
email?: string;
readonly createdAt: Date;
}
type Point = {
x: number;
y: number;
};
| 维度 | interface | type |
|---|---|---|
| 声明合并 | 同名可合并 | 不可合并,重复声明报错 |
| 联合 / 交叉 | 不能直接表达 | 可以 |
| 映射 / 条件类型 | 不能 | 可以 |
| 实现(implements) | 可以 | 可以(对象类型) |
| 性能 | 通常更好 | 复杂组合可能更慢 |
选择建议:
- 描述对象结构、需要被类实现或对外暴露的 API:用
interface。 - 需要联合、交叉、映射类型、别名:用
type。
扩展:
interface Admin extends User {
permissions: string[];
}
type AdminWithType = User & { permissions: string[] };
// 索引签名:任意字符串键
interface Dict {
[key: string]: number;
}
// 索引 + 已知键:已知键的类型必须是索引类型的子类型
interface Mixed {
length: number;
[key: string]: number;
}
五、类
class Person {
// 参数属性:在构造函数参数上加修饰符,自动声明并赋值
constructor(
public readonly name: string,
private age: number,
protected email?: string
) {}
greet(): string {
return `Hello, ${this.name}`;
}
// getter / setter
get info(): string {
return `${this.name} (${this.age})`;
}
// 静态成员
static create(name: string, age: number): Person {
return new Person(name, age);
}
}
访问修饰符:
| 修饰符 | 可见范围 |
|---|---|
public |
任意位置(默认) |
protected |
类内部与子类 |
private |
仅类内部(编译期检查,运行时仍可访问) |
readonly |
只能在声明或构造函数中赋值 |
抽象类与实现:
abstract class Shape {
abstract area(): number;
describe(): string {
return `面积 ${this.area()}`;
}
}
class Circle extends Shape {
constructor(private radius: number) {
super();
}
area(): number {
return Math.PI * this.radius ** 2;
}
}
interface Serializable {
toJSON(): string;
}
class Point implements Serializable {
constructor(public x: number, public y: number) {}
toJSON(): string {
return JSON.stringify({ x: this.x, y: this.y });
}
}
私有字段:private 是编译期约束,运行时可以通过 obj["age"] 访问。需要运行时私有用 #field:
class Counter {
#count = 0;
increment(): void {
this.#count += 1;
}
}
六、函数与泛型
6.1 函数类型
type Handler = (event: Event) => void;
type AsyncHandler = (id: string) => Promise<void>;
interface Api {
list(page?: number): Promise<string[]>;
}
// 可选参数、默认参数、剩余参数
function greet(name: string, greeting = "你好", ...tags: string[]): string {
return `${greeting} ${name} ${tags.join(",")}`;
}
// 函数重载
function parseValue(value: string): string;
function parseValue(value: number): number;
function parseValue(value: string | number): string | number {
return typeof value === "string" ? value.trim() : value;
}
this 类型:
function handler(this: HTMLElement, event: Event): void {
this.classList.add("active");
}
6.2 泛型
function identity<T>(value: T): T {
return value;
}
function first<T>(list: readonly T[]): T | undefined {
return list[0];
}
// 泛型约束
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 默认类型参数
interface Paginated<T, M = { total: number }> {
items: T[];
meta: M;
}
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
}
七、联合、交叉与类型收窄
type Result = { ok: true; data: string } | { ok: false; error: string };
function handle(result: Result): string {
if (result.ok) {
return result.data; // 收窄为成功分支
}
return result.error;
}
收窄手段:
| 手段 | 示例 |
|---|---|
typeof |
typeof x === "string" |
| 真值判断 | if (x) 排除 null/undefined/""/0 |
| 相等判断 | if (x === null) |
in 运算符 |
if ("error" in result) |
instanceof |
if (err instanceof Error) |
| 自定义守卫 | function isUser(v: unknown): v is User |
| 字面量判别 | 可辨识联合(ok: true / false) |
// 自定义类型守卫
function isUser(value: unknown): value is { id: number; name: string } {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as { id: unknown }).id === "number"
);
}
交叉类型会合并成员,同名属性类型不兼容时得到 never:
type A = { id: number };
type B = { id: string };
type C = A & B; // id: never
八、字面量类型与 as const
type Direction = "up" | "down";
type StatusCode = 200 | 404 | 500;
// as const:把值推断为最窄的字面量类型,并变为 readonly
const config = {
mode: "production",
port: 3000,
features: ["a", "b"]
} as const;
// typeof 取类型
type Config = typeof config;
// { readonly mode: "production"; readonly port: 3000; readonly features: readonly ["a", "b"] }
as const 常用于:
- 配置对象(保留字面量类型,用于
keyof typeof)。 - 数组转元组(
const pair = [1, "a"] as const)。 - 路由表、常量枚举的替代方案。
const ROUTES = {
home: "/",
user: "/users/:id"
} as const;
type RouteName = keyof typeof ROUTES;
枚举对比:
enum Color {
Red = "red",
Green = "green"
}
// 常量对象 + 联合类型(推荐,无运行时残留)
const Color2 = { Red: "red", Green: "green" } as const;
type ColorValue = (typeof Color2)[keyof typeof Color2];
数字枚举会生成双向映射对象,增加体积;字符串枚举无此问题但仍会产生运行时代码。能用常量对象就用常量对象。
九、工具类型
| 工具类型 | 作用 | 示例 |
|---|---|---|
Partial<T> |
全部属性可选 | Partial<User> |
Required<T> |
全部属性必填 | Required<User> |
Readonly<T> |
全部属性只读 | Readonly<User> |
Pick<T, K> |
选取部分属性 | Pick<User, "id" | "name"> |
Omit<T, K> |
排除部分属性 | Omit<User, "email"> |
Record<K, V> |
构造键值对象 | Record<string, number> |
Exclude<T, U> |
从联合中排除 | Exclude<"a" | "b", "a"> → "b" |
Extract<T, U> |
从联合中提取 | Extract<"a" | "b", "b"> → "b" |
NonNullable<T> |
去除 null/undefined | NonNullable<string | null> |
ReturnType<F> |
函数返回值类型 | ReturnType<typeof fn> |
Parameters<F> |
函数参数元组 | Parameters<typeof fn> |
Awaited<T> |
Promise 解包 | Awaited<Promise<string>> |
InstanceType<C> |
实例类型 | InstanceType<typeof Person> |
ConstructorParameters<C> |
构造参数元组 | ConstructorParameters<typeof Person> |
ThisParameterType / OmitThisParameter |
this 参数处理 | 较少使用 |
组合示例:
interface User {
id: number;
name: string;
email: string;
password: string;
}
type PublicUser = Omit<User, "password">;
type UserPatch = Partial<Pick<User, "name" | "email">>;
type UserMap = Record<number, PublicUser>;
Omit在联合类型上会"摊平"成对象,需要先分配(Omit<T, K>用条件类型分发)才能保持联合结构,这是常见的坑。
十、高级类型
10.1 keyof 与索引访问
interface User {
id: number;
name: string;
}
type Keys = keyof User; // "id" | "name"
type IdType = User["id"]; // number
type ValueType = User[keyof User]; // string | number
10.2 映射类型
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type Optional<T> = { [K in keyof T]?: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };
// 键重映射(4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
10.3 条件类型与 infer
type IsString<T> = T extends string ? true : false;
type ElementType<T> = T extends (infer U)[] ? U : never;
type UnwrapPromise<T> = T extends Promise<infer U> ? U : never;
type FunctionArgs<T> = T extends (...args: infer A) => unknown ? A : never;
type FunctionReturn<T> = T extends (...args: never[]) => infer R ? R : never;
条件类型对联合类型会分发:
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
阻止分发:用元组包裹。
type ToArrayNoDistribute<T> = [T] extends [unknown] ? T[] : never;
10.4 模板字面量类型
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
// 提取字符串模式
type ExtractId<T> = T extends `id:${infer Id}` ? Id : never;
type X = ExtractId<"id:42">; // "42"
内置字符串工具:Uppercase、Lowercase、Capitalize、Uncapitalize。
10.5 可辨识联合与状态机
type State =
| { status: "idle" }
| { status: "loading"; startedAt: number }
| { status: "success"; data: string[] }
| { status: "error"; message: string };
function render(state: State): string {
switch (state.status) {
case "idle":
return "未开始";
case "loading":
return `加载中 ${state.startedAt}`;
case "success":
return state.data.join(",");
case "error":
return `错误:${state.message}`;
}
}
这是实际项目中最有价值的写法:用类型把"哪些状态组合合法"表达出来。
十一、模块与模块解析
// 具名导出
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14;
// 默认导出
export default class Client {}
// 导入
import Client, { add, PI } from "./math";
import * as math from "./math";
import type { User } from "./types"; // 只导入类型,编译后消失
关键编译选项:
| 选项 | 作用 | 常见取值 |
|---|---|---|
module |
输出模块格式 | commonjs(Node)、ESNext(构建工具) |
moduleResolution |
解析策略 | node / node16 / bundler |
esModuleInterop |
允许默认导入 CommonJS 模块 | true |
resolveJsonModule |
支持导入 JSON | true |
allowSyntheticDefaultImports |
类型层面允许默认导入 | 随 esModuleInterop |
baseUrl + paths |
路径别名 | "@/*": ["src/*"] |
路径别名:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
}
}
paths 只影响类型检查,运行时不会改写路径。Node 运行时需要 tsconfig-paths 或打包器支持。
十二、tsconfig 配置详解
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2022", "DOM"],
"module": "commonjs",
"moduleResolution": "node",
"outDir": "dist",
"rootDir": "src",
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"exactOptionalPropertyTypes": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"sourceMap": true,
"declaration": false,
"incremental": true,
"isolatedModules": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
重点选项说明:
| 选项 | 说明 | 建议 |
|---|---|---|
target |
输出 JS 语法版本 | 与运行时一致,Node 20 可用 ES2022 |
lib |
可用的内置类型库 | 浏览器加 DOM,Node 不加 |
strict |
一组严格检查的总开关 | 必开 |
noUncheckedIndexedAccess |
索引访问结果包含 undefined |
建议开,能发现越界 |
noImplicitOverride |
重写父类方法必须写 override |
建议开 |
exactOptionalPropertyTypes |
?: 与 | undefined 区分 |
谨慎开,可能影响第三方类型 |
isolatedModules |
每个文件可独立编译 | 用 esbuild/swc/vite 时必开 |
skipLibCheck |
跳过 .d.ts 检查 |
建议开,否则第三方类型报错难处理 |
declaration |
生成 .d.ts |
发布库时开 |
incremental |
增量编译缓存 | 大项目开 |
isolatedModules 开启后的两个约束:
- 类型必须用
import type导入,否则编译后会残留导入。 export { SomeType }会报错,需改成export type { SomeType }。
十三、类型声明文件
// types/global.d.ts
declare global {
interface Window {
__APP_CONFIG__: {
apiBase: string;
};
}
}
export {};
给第三方无类型模块补充声明:
// types/legacy-lib.d.ts
declare module "legacy-lib" {
export function parse(input: string): Record<string, unknown>;
}
引用方式:
- 放在
include范围内。 - 或在代码里
/// <reference types="./types/global" />。
@types/* 包的来源:DefinitelyTyped;安装后自动生效,无需引用。
常见缺失类型处理顺序:
- 先找官方类型包(
@types/xxx)。 - 没有再手写
.d.ts。 - 最后才用
// @ts-expect-error(比@ts-ignore好,因为用到时会报错提醒)。
十四、装饰器
TS 5.0 起支持标准装饰器(Stage 3),与历史上的 experimentalDecorators 不是同一套。NestJS、TypeORM 等框架目前仍使用旧版装饰器,因此需要:
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
emitDecoratorMetadata 会输出设计类型的元数据,依赖注入才能按类型解析参数。
旧版装饰器示例:
function Log(target: unknown, key: string, descriptor: PropertyDescriptor) {
const original = descriptor.value as (...args: unknown[]) => unknown;
descriptor.value = function (...args: unknown[]) {
const start = Date.now();
const result = original.apply(this, args);
console.log(`${key} 耗时 ${Date.now() - start}ms`);
return result;
};
return descriptor;
}
class Service {
@Log
run(): void {
// ...
}
}
标准装饰器写法不同((value, context) => value),在纯前端代码中可用;与 Nest 混用时要注意编译选项冲突。
十五、常见编译错误对照
| 错误码 | 含义 | 常见原因与处理 |
|---|---|---|
| TS2307 | 找不到模块 | 路径错、缺少类型包、moduleResolution 不匹配 |
| TS2322 | 类型不可赋值 | 检查类型定义;必要时收窄而非断言 |
| TS2345 | 参数类型不匹配 | 函数签名与调用方不一致 |
| TS2339 | 属性不存在 | 联合类型未收窄,或类型定义缺少字段 |
| TS2531 / TS2532 | 可能为 null / undefined | 判空或可选链,不要滥用 ! |
| TS7006 | 参数隐式 any | 补类型,或开启 noImplicitAny 后逐个修复 |
| TS7053 | 索引访问类型不明确 | 用 Record<K, V> 或类型守卫 |
| TS18046 | 类型为 unknown | 先收窄再使用 |
| TS2304 | 找不到名称 | 缺少 import 或 lib/types 配置 |
| TS1208 | 文件不是模块 | 有顶层 import/export,或改用 .d.ts |
| TS2580 | 找不到 Node 全局 | 安装 @types/node |
| TS5055 | 输出文件会覆盖输入 | outDir 与源码目录重叠 |
排查顺序:
- 看错误码对应的是"类型不兼容"还是"模块解析失败"。
- 模块解析问题先看
include、baseUrl、paths、moduleResolution。 - 类型不兼容先打印类型:在出错处写
const _debug: never = value;会提示实际类型。 - 实在无法修复时用
@ts-expect-error并写明原因,避免@ts-ignore。
十六、FAQ
Q1:any 和 unknown 有什么区别?
any 关闭检查,可以随意使用;unknown 必须收窄后才能使用。外部数据(JSON、catch 变量)应使用 unknown。
Q2:interface 和 type 选哪个?
对象结构与对外 API 用 interface;需要联合、交叉、映射等类型运算用 type。
Q3:类型断言安全吗?
不安全。它只是告诉编译器"按这个类型处理",不做运行时校验。来自外部的数据要先校验再用。
Q4:为什么 ts-node 跑得快但类型检查没生效?
ts-node 默认做转译不做完整类型检查,用 tsc --noEmit 单独检查,或开启 ts-node --type-check。
Q5:strict 太严格,旧项目怎么迁移?
逐个开启子项(先 strictNullChecks,再 noImplicitAny),配合 // @ts-expect-error 逐步收敛。
Q6:enum 该不该用?
优先用常量对象 + 联合类型。数字枚举会产生双向映射,且与 isolatedModules 有冲突。
Q7:paths 配了但运行时找不到模块?
paths 只影响类型检查,运行时需要 tsconfig-paths、构建工具别名或 Node 的 subpath imports。
Q8:为什么开 isolatedModules 后报错?
类型导入必须用 import type;重新导出类型用 export type。
Q9:装饰器怎么选新旧两套?
使用 NestJS、TypeORM、Angular 等框架时用旧版(experimentalDecorators);纯前端新代码可用标准装饰器。同一项目不要混用。
Q10:编译慢怎么办?
开启 incremental、skipLibCheck;拆分 include;用 tsc --extendedDiagnostics 找耗时点;超大项目考虑 project references。
附录:类型速查
// 原始与容器
string number boolean bigint symbol null undefined
object any unknown never void
T[] Array<T> [A, B] readonly T[]
// 组合
A | B 联合
A & B 交叉
keyof T 键集合
T[K] 索引访问
typeof x 取值的类型
// 工具类型
Partial<T> Required<T> Readonly<T> Pick<T, K> Omit<T, K>
Record<K, V> Exclude<T, U> Extract<T, U> NonNullable<T>
ReturnType<F> Parameters<F> Awaited<T> InstanceType<C>
// 条件与映射
T extends U ? X : Y
{ [K in keyof T]: T[K] }
T extends (infer U)[] ? U : never
`prefix${string}`
// 断言与守卫
as T
satisfies T // 校验类型但保留窄类型(4.9+)
x is T // 类型谓词