1. 什么是 TypeScript
TypeScript(TS)是由微软开发并开源的 JavaScript 超集。"超集"意味着所有合法的 JS 代码都是合法的 TS 代码,你可以渐进式地迁移现有项目。
核心特征
- 静态类型:变量、参数、返回值都可以标注类型,编译器在编译期检查类型错误,而不是等到运行时崩溃
- 编译到 JS:浏览器不认识 TS,TS 文件经
tsc(TypeScript 编译器)编译后生成纯 JS - 增强的 IDE 支持:类型信息让编辑器能提供精准的自动补全、参数提示、重构支持
- 渐进式:可以用
any逐步迁移,不必一次全部类型化
为什么前端项目要用 TS
// 没有 TypeScript:运行时才发现错误
function greet(user) {
return `Hello, ${user.naem}!`; // 拼写错误 naem,运行时才报 undefined
}
// 有 TypeScript:编写时就发现错误
interface User { name: string; age: number; }
function greet(user: User) {
return `Hello, ${user.naem}!`; // TS 错误: Property 'naem' does not exist
}
2. 基础类型
TypeScript 的基础类型与 JavaScript 的运行时类型对应,但增加了几个重要的编译期专有类型。
原始类型
let name: string = 'Alice';
let age: number = 25; // 整数和浮点数都是 number
let active: boolean = true;
let nothing: null = null;
let undef: undefined = undefined;
let id: symbol = Symbol('uid');
let bigNum: bigint = 9007199254740991n;
unknown vs any(重要!)
any 关闭了类型检查——你可以对 any 类型的值做任何操作,TS 不会报错。unknown 是"类型安全的 any"——你必须先做类型检查(缩窄),才能操作它。
// any —— 逃生舱,但很危险
let data: any = fetchSomething();
data.foo().bar.baz(); // TS 完全不检查,运行时可能崩溃
// unknown —— 安全的"不知道是什么类型"
let value: unknown = fetchSomething();
value.toUpperCase(); // ❌ TS 错误:必须先检查类型
// 必须先缩窄类型才能使用
if (typeof value === 'string') {
value.toUpperCase(); // ✅ 现在知道是 string 了
}
原则:尽量不用 any。接收外部数据(API 响应、用户输入)时用 unknown,然后用类型守卫缩窄。在代码库中大量使用 any 会让 TypeScript 的价值大打折扣。
never 类型
never 表示"永远不会发生的值"。函数抛出异常或无限循环时,返回值类型是 never。在穷举检查时也很有用。
// 永远不会正常返回的函数
function fail(msg: string): never {
throw new Error(msg);
}
// 穷举检查 —— 如果有遗漏的 case,TS 会报错
type Shape = 'circle' | 'square' | 'triangle';
function getArea(shape: Shape): number {
switch (shape) {
case 'circle': return Math.PI;
case 'square': return 1;
case 'triangle': return 0.5;
default:
const exhausted: never = shape; // 如果漏掉某个 case,此处报错
throw new Error(`Unknown: ${exhausted}`);
}
}
3. 类型注解与类型推断
TypeScript 具备强大的类型推断能力——很多时候不需要手动标注,编译器能自动推断出类型。了解什么时候需要显式注解,什么时候可以省略,是写出"地道 TS"的关键。
// 可以推断 —— 无需注解
const name = 'Alice'; // 推断为 string
const age = 25; // 推断为 number (字面量类型 25)
const arr = [1, 2, 3]; // 推断为 number[]
const obj = { x: 1, y: 2 }; // 推断为 { x: number; y: number }
// 需要显式注解的情况
// 1. 函数参数(无法推断)
function add(a: number, b: number): number { return a + b; }
// 2. 初始值为 null/undefined(后续赋值时需要知道类型)
let user: User | null = null;
// 3. 需要明确比推断结果更宽的类型
let id: string | number = 1; // 推断会是 1(字面量),但我们想允许 string
// 4. 复杂对象(提高可读性)
const config: AppConfig = {
apiUrl: 'https://api.example.com',
timeout: 5000,
};
4. 接口(Interface)
接口用于定义对象的"形状"——它有哪些属性,每个属性是什么类型。这是 TypeScript 中描述数据结构最常用的方式。
基本接口与可选/只读属性
interface User {
id: number;
name: string;
email: string;
age?: number; // ? 表示可选属性(可以不存在)
readonly createdAt: Date; // readonly 表示只读,赋值后不可修改
}
const user: User = {
id: 1,
name: 'Alice',
email: 'alice@example.com',
createdAt: new Date(),
};
user.name = 'Bob'; // ✅
user.createdAt = new Date(); // ❌ Cannot assign to 'createdAt' (readonly)
索引签名
当你不知道对象有哪些属性键,但知道值的类型时,使用索引签名。
interface StringMap {
[key: string]: string; // 任意 string 键,值必须是 string
}
interface Scores {
[studentId: string]: number;
total: number; // 可以和索引签名共存,但类型必须兼容
}
接口继承
interface Animal {
name: string;
eat(): void;
}
interface Pet extends Animal {
owner: string;
play(): void;
}
// 可以继承多个接口
interface ServiceDog extends Animal, Pet {
certification: string;
}
接口 vs 类型别名(Interface vs Type Alias)
| 特性 | Interface | Type Alias |
|---|---|---|
| 定义对象形状 | ✅ 主要用途 | ✅ 可以 |
| 继承 / 扩展 | ✅ extends | ✅ 交叉类型 & |
| 声明合并 | ✅ 同名 interface 自动合并 | ❌ 不支持 |
| 联合类型 | ❌ 不支持 | ✅ A | B |
| 映射类型 / 条件类型 | ❌ | ✅ |
| 推荐场景 | 对象、类、API 响应 | 联合、交叉、复杂类型 |
5. 类型别名(Type Alias)
使用 type 关键字创建类型的"别名",可以给任何类型起一个有意义的名字。
字面量类型与联合类型
// 字面量类型 —— 只能是这几个值
type Direction = 'north' | 'south' | 'east' | 'west';
type Status = 'pending' | 'fulfilled' | 'rejected';
type HttpCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;
// 联合类型 —— 可以是其中任意一种类型
type ID = string | number;
type Nullable<T> = T | null;
function move(direction: Direction) { /* ... */ }
move('north'); // ✅
move('up'); // ❌ Argument of type '"up"' is not assignable
交叉类型
type Timestamped = { createdAt: Date; updatedAt: Date };
type SoftDeletable = { deletedAt: Date | null };
// 交叉类型 —— 同时满足所有类型的属性
type Post = {
title: string;
content: string;
} & Timestamped & SoftDeletable;
// Post 必须同时有 title, content, createdAt, updatedAt, deletedAt
6. 泛型(Generics)
泛型让你编写可以处理多种类型的代码,同时保持类型安全。把泛型参数理解为"类型的占位符"——使用时再传入具体类型。
泛型函数
// 没有泛型 —— 只能处理 string
function firstString(arr: string[]): string | undefined {
return arr[0];
}
// 有泛型 T —— 输入什么类型的数组,返回同类型
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const s = first(['a', 'b']); // 推断 T = string,返回 string | undefined
const n = first([1, 2, 3]); // 推断 T = number,返回 number | undefined
// 多个类型参数
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
泛型接口与约束
// 泛型接口
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
type UserResponse = ApiResponse<User>;
type PostListResponse = ApiResponse<Post[]>;
// 泛型约束(T 必须有 id 属性)
function findById<T extends { id: number }>(
items: T[],
id: number
): T | undefined {
return items.find(item => item.id === id);
}
// 约束 K 必须是 T 的键(keyof)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
内置泛型工具类型
| 工具类型 | 作用 | 示例 |
|---|---|---|
Partial<T> | 所有属性变可选 | Partial<User> |
Required<T> | 所有属性变必填 | Required<Options> |
Pick<T, K> | 只保留指定属性 | Pick<User, 'id' | 'name'> |
Omit<T, K> | 排除指定属性 | Omit<User, 'password'> |
Record<K, V> | 键为 K、值为 V 的对象 | Record<string, number> |
ReturnType<F> | 获取函数返回值类型 | ReturnType<typeof fetchUser> |
// Partial 常用于更新操作(只需要传部分字段)
function updateUser(id: number, data: Partial<User>) {
// data 中的所有属性都是可选的
}
updateUser(1, { name: 'Bob' }); // 只更新 name,合法
// Omit 用于创建"不含密码的用户"类型
type PublicUser = Omit<User, 'password' | 'secretKey'>;
7. 类型守卫(Type Guards)
类型守卫是在运行时缩窄(narrow)类型的手段——通过条件判断让 TypeScript 知道在某个代码分支中,变量是什么具体类型。
type StringOrNumber = string | number;
function process(value: StringOrNumber) {
// typeof 类型守卫
if (typeof value === 'string') {
// 此分支中 value 是 string
console.log(value.toUpperCase());
} else {
// 此分支中 value 是 number
console.log(value.toFixed(2));
}
}
// instanceof 类型守卫
function handleError(err: unknown) {
if (err instanceof Error) {
console.log(err.message); // err 是 Error 类型
}
}
// in 类型守卫(检查属性是否存在)
interface Cat { meow(): void }
interface Dog { bark(): void }
function makeSound(animal: Cat | Dog) {
if ('meow' in animal) {
animal.meow(); // animal 是 Cat
} else {
animal.bark(); // animal 是 Dog
}
}
// 自定义类型谓词(is)
function isString(value: unknown): value is string {
return typeof value === 'string';
}
const items: unknown[] = ['hello', 42, true, 'world'];
const strings = items.filter(isString); // 推断为 string[]
8. 枚举(Enum)
枚举用于定义一组命名的常量,让代码更易读。TypeScript 支持数字枚举和字符串枚举。
// 数字枚举(默认从 0 开始递增)
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right, // 3
}
// 字符串枚举(推荐 —— 调试更友好)
enum Status {
Pending = 'PENDING',
Active = 'ACTIVE',
Inactive = 'INACTIVE',
}
function handleStatus(s: Status) {
if (s === Status.Active) {
console.log('Active!');
}
}
// const enum —— 编译后直接内联值,不生成 JS 对象(性能优化)
const enum HttpMethod {
GET = 'GET',
POST = 'POST',
DELETE = 'DELETE',
}
现代趋势:很多项目用字面量联合类型代替枚举(如 type Status = 'pending' | 'active' | 'inactive'),因为更简洁、与 JSON 数据自然对应。两种方式都可以,了解即可。
9. React 中的 TypeScript 实战
在实际的 React 项目中,TypeScript 最常用于:Props 类型定义、Hook 泛型、事件类型。
组件 Props 类型
// 定义 Props 接口
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
children?: React.ReactNode; // 接受任意 React 内容
}
// 函数组件 —— 推荐省略 React.FC,直接用 props 类型
function Button({
label,
onClick,
variant = 'primary',
disabled = false
}: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
事件类型
function Form() {
// input onChange 事件
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};
// button onClick 事件
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
e.preventDefault();
};
// form onSubmit 事件
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
};
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>Submit</button>
</form>
);
}
useState 与 useRef 泛型
import { useState, useRef } from 'react';
function Counter() {
// useState 泛型 —— 通常可以推断,复杂类型需要显式指定
const [count, setCount] = useState<number>(0);
const [user, setUser] = useState<User | null>(null);
// useRef 泛型 —— 指定 DOM 元素类型
const inputRef = useRef<HTMLInputElement>(null);
const focusInput = () => {
inputRef.current?.focus(); // current 可能是 null,用 ?. 安全调用
};
return (
<div>
<input ref={inputRef} />
<button onClick={focusInput}>Focus</button>
</div>
);
}
本章小结:TypeScript 的核心价值在于类型安全和开发体验。重点掌握:Interface/Type 定义数据结构、泛型写可复用代码、类型守卫缩窄联合类型、React 中的 Props 和事件类型。