TypeScript 完全指南:从入门到精通
告别“AnyScript”,用 TS 写出更健壮的代码
前言:为什么我们需要 TS?
如果你是一名前端开发者,大概率经历过这样的场景:
// 一段看似正常的 JS 代码
function greet(user) {
return "Hello, " + user.name;
}
greet(null); // 💥 程序炸了:Cannot read property 'name' of null
这种在运行时才暴露错误的问题,在 JavaScript 中太常见了。JS 最初的定位是简单的浏览器脚本语言,代码量很小。但如今,JS 已经可以全栈开发,代码量呈指数级增长,当年“出生简陋”的 JS,在面对大型应用时,就显得力不从心了。
TypeScript 的出现,就是为了解决 JavaScript 的“痛点”:
- 不清不楚的数据类型:你永远不知道一个变量到底是个啥。
- 逻辑上的漏洞:比如上面那个例子,漏掉了空值判断。
- 访问不存在的属性:对象里根本没这个 key,你却偏要访问。
- 低级的拼写错误:把
onClick写成onClcik,找半天 bug。
TS 的核心价值在于:它提供了一套静态类型检查系统,在代码运行之前(编译阶段)就帮你发现这些潜在的错误。
说白了,TS 就是 JS 的“盔甲”,穿上它,你写的代码会更健壮,重构起来也更有底气。
第一章:快速上手 TS 环境
在开始写 TS 之前,我们需要先搭建好环境。
1.1 安装 TypeScript
TypeScript 最终会被编译成 JavaScript,所以我们需要一个编译器。
# 全局安装 TypeScript
npm i typescript -g
# 检查是否安装成功
tsc -v
1.2 编译与配置
安装完成后,我们就可以使用 tsc 命令来编译 .ts 文件了。
# 编译一个 ts 文件
tsc index.ts
# 初始化 tsconfig.json 配置文件,控制 tsc 的行为
tsc --init
# 监听文件变化,实时编译
tsc --watch
小技巧: 默认情况下,即使 TS 代码里有类型错误,tsc 也会强行编译成 JS 文件。如果你希望在有类型错误时阻止编译,可以在 tsconfig.json 中开启 noEmitOnError 选项:
{
"compilerOptions": {
"noEmitOnError": true
}
}
第二章:TS 的核心——类型系统
类型系统是 TS 的精髓所在,掌握它,你就掌握了 TS 的 80%。
2.1 基础类型与类型声明
TS 支持与 JS 几乎相同的数据类型,并额外增加了一些新类型。
// 基本类型声明
let a: string;
let b: number;
let c: boolean;
// 字面量类型:值本身即是类型,不能更改
let hello: 'hello';
hello = 'world'; // ❌ 报错:不能将类型'"world"'分配给类型'"hello"'
// 类型推论:TS 能根据赋值自动推导类型
let d = 99; // TS 自动推导 d 为 number 类型
// d = 'hello'; // ❌ 报错:类型不匹配
2.2 any vs unknown vs never
这三个类型是 TS 新增的,也是初学者最容易混淆的地方。
-
any:类型系统的“黑洞”
它可以赋值给任意类型,也可以被任意类型赋值。不推荐使用,因为它会完全失去类型检查,让 TS 变成了“AnyScript”。 -
unknown:类型安全的“any”
它可以存储任意类型的值,但不能直接赋值给其他类型,需要先进行类型断言或类型守卫。let unknownVar: unknown; let strVar: string; // strVar = unknownVar; // ❌ 报错:unknown 类型不能赋值给 string strVar = unknownVar as string; // ✅ 使用类型断言 strVar = <string>unknownVar; // ✅ 另一种断言写法 -
never:表示“永远不存在”的值
通常用于函数抛出异常或死循环。function throwError(msg: string): never { throw new Error(msg); } // 几乎用不到,因为它更多是 TS 内部使用,比如做完整性检查。
金句: any 是魔鬼,unknown 是保镖。能用 unknown 就别用 any。
2.3 void 与函数返回值
void 表示函数没有返回值。但有一个小细节需要注意:
type VoidFunc = () => void;
let f1: VoidFunc = function() {
return 666; // ✅ 允许!但返回值会被忽略
};
let f2: VoidFunc = () => 123; // ✅ 也允许!箭头函数简写隐式返回
// 虽然可以返回其他值,但不建议依赖这个返回值
const result = f1(); // result 类型是 void,而不是 number
为什么 TS 不强制 void 函数必须返回 undefined? 这是为了兼容 JS 的习惯,比如数组的 forEach 中,你经常写 () => arr.push(1),这其实隐式返回了一个数字,但没人会去用这个返回值。
2.4 对象、数组与函数类型声明
-
对象类型
// 固定属性 let person: { name: string, age?: number }; // ? 表示可选属性 // 索引签名:属性名不确定 let dict: { [key: string]: any }; dict = { name: 'lgl', gender: 'male' }; // 注意:object 和 Object 的区别 let a: object; // 只能存非原始类型(对象、数组等) let b: Object; // 可以调用 Object 方法的类型,不能存 null、undefined // 实际开发中这两个很少直接用,更推荐上面那种字面量或接口方式 -
数组类型
let arr1: number[] = [1, 2, 3]; let arr2: Array<number> = [1, 2, 3]; // 泛型写法 -
函数类型
// 声明一个函数类型 let count: (x: number, y: number) => number; count = (x, y) => x + y;
2.5 元组(Tuple)与枚举(Enum)
-
元组:固定长度、固定类型的数组。
let tuple: [string, number] = ['hello', 123]; // tuple = [123, 'hello']; // ❌ 报错:顺序不匹配 -
枚举:一组命名常量,增强代码可读性。
enum Color { Red, // 默认 0 Green, // 默认 1 Blue // 默认 2 } console.log(Color.Red); // 0 // 如果不想生成多余的 JS 代码,可以使用常量枚举(编译后直接内联替换) const enum Gender { Male = '男', Female = '女' } let myGender = Gender.Male; // 编译后直接变成 '男'
2.6 type 与接口(Interface)的灵活用法
-
联合类型与交叉类型
// 联合类型:或 type Status = number | string; type Gender = '男' | '女'; // 交叉类型:且 type Person = { name: string } & { age: number }; let p: Person = { name: 'lgl', age: 18 }; -
类型别名 vs 接口
type可以声明联合类型、交叉类型等,更灵活。interface更适合声明对象的形状,且可以重复声明合并。
interface User { name: string; } interface User { age: number; } // User 最终类型为 { name: string; age: number; }
第三章:面向对象——类(Class)与抽象类
TS 对 ES6 的类进行了增强,提供了完整的面向对象支持。
3.1 类的构造器简写
传统的写法比较冗余:
class Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
一键简写: 在构造器参数前加上访问修饰符(public、private、protected),TS 会自动帮我们定义并赋值。
class Person {
constructor(public name: string, public age: number) {}
}
const p = new Person('lgl', 18);
console.log(p.name); // 直接访问
3.2 抽象类(Abstract Class)
抽象类是用来被继承的,它不能实例化,可以包含具体实现,也可以包含抽象方法(只有定义,没有实现)。
abstract class Animal {
abstract makeSound(): void; // 抽象方法,子类必须实现
move(): void {
console.log('roaming the earth...');
}
}
class Dog extends Animal {
makeSound(): void {
console.log('汪汪汪!');
}
}
// const animal = new Animal(); // ❌ 报错:无法创建抽象类的实例
抽象类 vs 接口:
| 特性 | 抽象类 | 接口 |
|---|---|---|
| 是否包含实现代码 | ✅ 可以 | ❌ 不可以 |
| 继承/实现数量 | 只能继承一个 | 可以实现多个 |
| 适用场景 | 提供通用基类,复用代码 | 定义契约,描述结构 |
第四章:进阶——泛型(Generics)
泛型允许你在定义函数、类、接口时,不预先指定具体类型,而是在使用时再确定。这是一个极其强大的工具。
// 一个“万能”的 identity 函数
function identity<T>(arg: T): T {
return arg;
}
// 使用时显式指定类型
let output1 = identity<string>("myString");
// 或利用类型推论,自动推导
let output2 = identity(123); // output2 是 number 类型
泛型也可以用在类和接口中:
// 泛型类
class Stack<T> {
private items: T[] = [];
push(item: T) { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
}
const numStack = new Stack<number>();
numStack.push(1);
// numStack.push('a'); // ❌ 报错
// 泛型接口
interface KeyValue<K, V> {
key: K;
value: V;
}
const kv: KeyValue<string, number> = { key: 'age', value: 18 };
金句: 泛型让代码从“一劳永逸”变成“一劳多逸”,一次定义,多种类型复用。
第五章:装饰器(Decorator)——TS 的黑魔法
装饰器是 TS 在 2.0 版本引入的一个重要特性,它是一个特殊的函数,可以“装饰”类、方法、属性等。注意:装饰器是一项实验性特性,在 tsconfig.json 中需要开启 experimentalDecorators 选项。
记住一个口诀:类装饰器管构造,方法装饰器管行为,访问器管 get/set,属性管定义,参数管顺序。
5.1 类装饰器(Class Decorator)
应用场景:修改类的构造函数、添加静态属性、替换类本身。
入参:构造函数的构造函数(即 target: Function)。
// 场景:给类打上版本号标签
function addVersion(version: string) {
return function <T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
version = version;
// 还可以添加静态属性
static apiUrl = '/api/v2';
};
};
}
@addVersion('1.0.0')
class UserApi {
constructor(public name: string) {}
}
const user = new UserApi('lgl');
console.log((user as any).version); // '1.0.0'
console.log((UserApi as any).apiUrl); // '/api/v2'
5.2 方法装饰器(Method Decorator)
应用场景:日志记录、性能监控(@timer)、权限校验、修改返回值。
入参:(target, propertyKey, descriptor)。
// 场景:权限校验装饰器
function checkRole(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
// 模拟从上下文中获取当前用户
const currentUser = { name: 'lgl', role: 'admin' };
if (currentUser.role === role) {
return originalMethod.apply(this, args);
} else {
throw new Error(`权限不足,需要 ${role} 角色`);
}
};
return descriptor;
};
}
class OrderService {
@checkRole('admin')
deleteOrder(id: number) {
console.log(`订单 ${id} 已删除`);
}
}
const service = new OrderService();
service.deleteOrder(1001); // ✅ 正常执行
5.3 访问器装饰器(Accessor Decorator)
应用场景:拦截 Getter/Setter 的读写操作,比如数据劫持、防抖。
入参:与方法装饰器完全相同 (target, propertyKey, descriptor)。
// 场景:阻止属性被频繁修改(防抖 setter)
function debounceSet(wait: number) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalSet = descriptor.set!;
let timer: NodeJS.Timeout | null = null;
descriptor.set = function (value: any) {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
originalSet.call(this, value);
timer = null;
}, wait);
};
return descriptor;
};
}
class SearchBox {
private _keyword: string = '';
get keyword() { return this._keyword; }
@debounceSet(300)
set keyword(val: string) {
this._keyword = val;
console.log(`搜索关键词更新为: ${val}`); // 只会延迟 300ms 后输出最后一次
}
}
const box = new SearchBox();
box.keyword = 'a';
box.keyword = 'ab';
box.keyword = 'abc'; // 只有这次会在 300ms 后触发打印
5.4 属性装饰器(Property Decorator)
应用场景:为属性定义元数据、映射数据库字段、依赖注入(DI)。
入参:(target, propertyKey) —— 没有第三个参数! 且无法直接修改属性的值,通常配合 reflect-metadata 库使用。
// 场景:标记字段对应的数据库列名
function Column(name: string) {
return function (target: any, propertyKey: string) {
// 在原型上存储元数据
if (!target.constructor.__columns) {
target.constructor.__columns = {};
}
target.constructor.__columns[propertyKey] = name;
};
}
class UserEntity {
@Column('user_name')
name: string;
@Column('user_age')
age: number;
}
// 模拟 ORM 读取映射关系
console.log((UserEntity as any).__columns);
// { name: 'user_name', age: 'user_age' }
5.5 参数装饰器(Parameter Decorator)
应用场景:在 NestJS / Java Spring 中极为常见,用于获取路由参数 @Param('id')、注入服务等。
入参:(target, methodKey, parameterIndex)。
// 场景:标记参数为需要自动注入
function InjectUser(target: any, methodKey: string, parameterIndex: number) {
if (!target.__injectParams) {
target.__injectParams = {};
}
if (!target.__injectParams[methodKey]) {
target.__injectParams[methodKey] = [];
}
target.__injectParams[methodKey].push(parameterIndex);
}
class OrderController {
getOrders(@InjectUser userId: number) {
console.log(`查询用户 ${userId} 的订单`);
}
}
// 模拟框架在调用时发现 __injectParams,自动从 session 中取出 userId 填进去
5.6 装饰器执行顺序小贴士
当多个装饰器作用于同一个目标时,执行顺序为:
属性/方法/参数装饰器 先执行(从下到上) → 类装饰器 最后执行
// 执行顺序:参数装饰器 -> 方法装饰器 -> 类装饰器
@ClassDecorator
class Example {
@MethodDecorator
method(@ParamDecorator param: any) {}
}
第六章:TypeScript 高级类型编程(解锁“类型体操”)
只会写 interface 和 type 只是入门。真正的中级进阶,是学会操作类型,把类型当成“值”来处理。
6.1 工具类型(Utility Types)——TS 官方自带的“万能工具箱”
TypeScript 内置了几个超级好用的工具类型,直接帮我们省去大量重复定义。
| 工具类型 | 作用 | 实战场景 |
|---|---|---|
Partial<T> |
将 T 中所有属性变为可选 | 更新数据时,只传部分字段 |
Required<T> |
将 T 中所有属性变为必选 | 强制补全配置项 |
Pick<T, K> |
从 T 中挑选几个属性组成新类型 | 只取接口返回的某几个字段 |
Omit<T, K> |
从 T 中剔除几个属性组成新类型 | 排除敏感字段(如密码) |
Record<K, T> |
构造一个对象类型,键为 K,值为 T | 做字典映射 |
Readonly<T> |
将 T 中所有属性变为只读 | 防止对象被意外修改 |
interface User {
id: number;
name: string;
password: string;
email: string;
}
// 场景1:更新时,只用传 id 和想改的字段
type UpdateUserParams = Partial<Omit<User, 'id'>> & { id: number };
// 场景2:前端展示列表,去掉敏感 password
type PublicUser = Omit<User, 'password'>;
// 场景3:做个国家编码映射
type CountryMap = Record<string, string>;
const map: CountryMap = { CN: '中国', US: '美国' };
金句: Pick 和 Omit 是你处理庞大后端接口返回数据时的最佳战友,能极大增强前端的类型安全性。
6.2 映射类型(Mapped Types)——定制自己的工具类型
明白了 Partial 的原理,你就能创造自己的工具类型。
// 手写一个 Partial(原理揭秘)
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
// 手写一个 Readonly(只读)
type MyReadonly<T> = {
readonly [P in keyof T]: T[P];
};
// 进阶:给所有属性加上前缀(比如转成 getter 风格)
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P];
};
// 如果 T 有 name: string,会生成 getName: () => string
6.3 条件类型(Conditional Types)——类型的“if/else”
条件类型让类型具备逻辑判断能力,语法是 T extends U ? X : Y。
// 场景:根据泛型自动推断返回的数组类型
type ElementType<T> = T extends any[] ? T[number] : T;
type A = ElementType<string[]>; // A 为 string
type B = ElementType<number>; // B 为 number
// 高级结合:过滤掉函数类型
type NonFunctionPropertyNames<T> = {
[K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
interface Mixed {
name: string;
age: number;
eat: () => void;
}
type OnlyProps = NonFunctionPropertyNames<Mixed>; // 'name' | 'age'
6.4 infer 关键字——类型推断的“占位符”
infer 是条件类型里的“占位符”,用于提取类型的某一部分。
// 提取函数返回值类型(手写 ReturnType)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() {
return { id: 1, name: 'lgl' };
}
type UserType = MyReturnType<typeof getUser>; // { id: number; name: string; }
// 提取数组元素类型
type MyArrayElement<T> = T extends (infer U)[] ? U : never;
type Str = MyArrayElement<string[]>; // string
6.5 类型守卫(Type Guards)——让 TS 在代码块内“变聪明”
有时候 TS 无法自动推断联合类型的具体分支,需要我们用 is 关键字来收窄类型。
// 自定义类型守卫
function isString(value: any): value is string {
return typeof value === 'string';
}
function process(input: string | number) {
if (isString(input)) {
// 在这个块里,TS 知道 input 一定是 string
console.log(input.toUpperCase());
} else {
console.log(input.toFixed(2));
}
}
// 另一种方式:typeof 类型守卫
function process2(input: string | number) {
if (typeof input === 'string') {
console.log(input.length); // TS 自动收窄为 string
}
}
6.6 内置高级类型补充
| 工具类型 | 作用 |
|---|---|
Parameters<T> |
获取函数 T 的参数类型(元组形式) |
ReturnType<T> |
获取函数 T 的返回值类型 |
InstanceType<T> |
获取构造函数 T 的实例类型 |
NonNullable<T> |
从 T 中剔除 null 和 undefined |
function greet(name: string, age: number): string {
return `${name} is ${age} years old`;
}
type GreetParams = Parameters<typeof greet>; // [string, number]
type GreetReturn = ReturnType<typeof greet>; // string
第七章:实战最佳实践总结
7.1 开发建议
- 始终开启
strict模式:在tsconfig.json中开启严格模式,它会帮你检查更多潜在问题。 - 避免使用
any:把它当作最后的手段,优先考虑unknown或更具体的类型。 - 善用类型推导:能自动推导出来的类型,就不用显式声明,保持代码简洁。
- 使用
interface定义对象形状,使用type定义联合类型。 - 泛型是你的好帮手:写可复用的组件、函数时,优先考虑泛型。
- 装饰器谨慎使用:虽然强大,但过度使用会降低代码可读性,适合框架层使用。
7.2 tsconfig.json 推荐配置(快速上手版)
{
"compilerOptions": {
"target": "ES2020",
"module": "commonjs",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"noEmitOnError": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
写在最后(高阶心法)
- 类型编程不是炫技:在团队协作中,可读性永远大于“骚操作”。如果写一个复杂的泛型能让调用方爽到,那就写;如果只是自己爽,别人看吐血,那请加上详细注释。
- 理解
keyof和in:keyof取键的联合类型,in遍历联合类型。这是映射类型的基石。 - TS 版本迭代很快:关注 TypeScript 官方 Release Notes,每个版本都会带来新工具类型和语法糖,保持学习。
TypeScript 就像是 JavaScript 的“静态显微镜”,语法是皮,类型思维是骨。当你习惯了用类型去“约束”和“推导”时,你就会发现,代码写完即完工,Bug 无处遁形。
更多推荐


所有评论(0)