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。

需要先明确三件事:

  1. 类型只在编译期存在。运行时没有任何类型检查,as 断言也不会做校验。
  2. 类型不改变运行时行为。interface、type、泛型在编译后全部消失。
  3. 类型的作用是提前暴露错误,不是"自动保证正确"。类型写错(例如滥用 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 开启后的两个约束:

  1. 类型必须用 import type 导入,否则编译后会残留导入。
  2. 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;安装后自动生效,无需引用。

常见缺失类型处理顺序:

  1. 先找官方类型包(@types/xxx)。
  2. 没有再手写 .d.ts。
  3. 最后才用 // @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 与源码目录重叠

排查顺序:

  1. 看错误码对应的是"类型不兼容"还是"模块解析失败"。
  2. 模块解析问题先看 include、baseUrl、paths、moduleResolution。
  3. 类型不兼容先打印类型:在出错处写 const _debug: never = value; 会提示实际类型。
  4. 实在无法修复时用 @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                   // 类型谓词
← 返回帖子 更多前端内容 →