第16期 | TypeScript:给代码加类型铠甲——让 Bug 死在编译阶段
·
第16期 | TypeScript:给代码加类型铠甲——让 Bug 死在编译阶段
🎯 今天你将学会
- 理解 TypeScript 解决的根本问题
- 掌握 TS 的核心类型系统:原始类型、对象类型、联合类型、泛型
- 在 React 组件中正确使用 TypeScript
- 用 AI 自动生成类型定义并帮你理解复杂泛型
📖 核心知识
1. TypeScript 解决什么问题
// JavaScript:运行时才报错
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}
getUser("abc"); // 运行时请求失败,甚至静默出错
// TypeScript:编译时就报错
function getUser(id: number): Promise<User> {
return fetch(`/api/users/${id}`).then(r => r.json());
}
getUser("abc"); // ❌ 编译报错:类型 'string' 不能赋值给类型 'number'
TypeScript 的本质:在代码运行之前,用类型系统发现错误。
TypeScript 的好处:
- 编辑器自动补全(知道对象有哪些属性)
- 重构安全(改了函数签名,所有调用处都会报错提醒)
- 文档即代码(类型注解就是最好的文档)
- 减少
undefined is not a function类运行时错误
2. 基础类型
// 原始类型
let name: string = '张三';
let age: number = 25;
let isActive: boolean = true;
let nothing: null = null;
let notDefined: undefined = undefined;
// 数组
let scores: number[] = [95, 87, 92];
let tags: string[] = ['react', 'typescript'];
let mixed: Array<string | number> = ['a', 1, 'b', 2]; // 泛型写法
// 元组(固定长度、固定类型的数组)
let point: [number, number] = [10, 20];
let nameAge: [string, number] = ['张三', 25];
// any:关闭类型检查(尽量少用)
let anything: any = 'hello';
anything = 42; // OK
// unknown:比 any 安全,使用前必须做类型检查
let userInput: unknown = getUserInput();
if (typeof userInput === 'string') {
console.log(userInput.toUpperCase()); // 安全
}
// never:永远不会返回的函数
function throwError(message: string): never {
throw new Error(message);
}
3. 对象类型与接口
// interface:定义对象的形状(推荐用于描述数据结构)
interface User {
id: number;
name: string;
email: string;
avatar?: string; // ? 表示可选
readonly createdAt: Date; // readonly:只读,不能修改
}
// 使用接口
const user: User = {
id: 1,
name: '张三',
email: 'zhangsan@example.com',
createdAt: new Date(),
};
// 接口继承
interface Admin extends User {
role: 'super_admin' | 'admin'; // 字面量联合类型
permissions: string[];
}
// type alias:类型别名(更灵活,可以定义联合类型)
type Status = 'loading' | 'success' | 'error' | 'idle';
type ID = string | number; // 联合类型
type Product = {
id: ID;
name: string;
price: number;
status: Status;
};
interface vs type 选择:
| 场景 | 推荐 |
|---|---|
| 描述对象/类的结构 | interface |
| 联合类型、交叉类型 | type |
| 需要扩展(继承)的数据模型 | interface |
| 函数类型 | 两者都行,type 更直观 |
4. 函数类型
// 参数类型 + 返回值类型
function add(a: number, b: number): number {
return a + b;
}
// 箭头函数类型
const multiply = (a: number, b: number): number => a * b;
// 可选参数和默认值
function greet(name: string, prefix?: string): string {
return `${prefix ?? '你好'},${name}!`;
}
// 函数类型作为参数(高阶函数)
type Callback = (error: Error | null, result?: string) => void;
function doTask(cb: Callback) {
try {
cb(null, '成功');
} catch (e) {
cb(e as Error);
}
}
// 函数重载(同名函数不同参数类型)
function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
return String(value);
}
5. 泛型:类型的"参数"
泛型让你写出可以适应多种类型的通用代码,不需要重复写相同逻辑。
// 没有泛型:要为每种类型写一遍
function firstString(arr: string[]): string { return arr[0]; }
function firstNumber(arr: number[]): number { return arr[0]; }
// 有泛型:一个函数搞定所有类型
function first<T>(arr: T[]): T {
return arr[0];
}
const s = first(['a', 'b', 'c']); // 推断为 string
const n = first([1, 2, 3]); // 推断为 number
const u = first<User>(users); // 明确指定泛型参数
常用泛型工具类型:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Partial:所有字段变可选(适合更新操作)
type UpdateUser = Partial<User>;
// 等价于:{ id?: number; name?: string; email?: string; password?: string; }
// Required:所有字段变必填
type RequiredUser = Required<User>;
// Pick:选取部分字段
type UserSummary = Pick<User, 'id' | 'name'>;
// 等价于:{ id: number; name: string; }
// Omit:排除某些字段(常用于不暴露密码)
type PublicUser = Omit<User, 'password'>;
// 等价于:{ id: number; name: string; email: string; }
// Record:定义键值对映射
type RoleMap = Record<string, string[]>;
// 等价于:{ [key: string]: string[] }
// ReturnType:获取函数返回值类型
function getUser() { return { id: 1, name: '张三' }; }
type UserType = ReturnType<typeof getUser>; // { id: number; name: string; }
6. 在 React 中使用 TypeScript
函数组件 Props 类型:
// 定义 Props 接口
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'secondary' | 'danger';
disabled?: boolean;
children?: React.ReactNode; // 接受任何 React 可渲染内容
}
// 函数组件
function Button({ label, onClick, variant = 'primary', disabled = false }: ButtonProps) {
return (
<button
className={`btn btn-${variant}`}
onClick={onClick}
disabled={disabled}
>
{label}
</button>
);
}
// 使用
<Button label="提交" onClick={() => console.log('click')} variant="primary" />
<Button label="删除" onClick={handleDelete} variant="danger" disabled={isLoading} />
useState 类型:
// 简单类型(TS 自动推断)
const [count, setCount] = useState(0); // number
const [name, setName] = useState(''); // string
const [isOpen, setIsOpen] = useState(false); // boolean
// 复杂类型(需要手动标注)
const [user, setUser] = useState<User | null>(null);
const [posts, setPosts] = useState<Post[]>([]);
const [status, setStatus] = useState<'loading' | 'success' | 'error'>('loading');
useRef 类型:
// 引用 DOM 元素:初始值为 null,类型是具体的 DOM 类型
const inputRef = useRef<HTMLInputElement>(null);
const divRef = useRef<HTMLDivElement>(null);
// 使用时需要判空(因为挂载前是 null)
function handleFocus() {
inputRef.current?.focus();
}
事件处理类型:
function Form() {
// React 事件类型
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
console.log(e.target.value);
}
function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();
// ...
}
function handleClick(e: React.MouseEvent<HTMLButtonElement>) {
console.log(e.clientX, e.clientY);
}
return (
<form onSubmit={handleSubmit}>
<input onChange={handleChange} />
<button onClick={handleClick}>提交</button>
</form>
);
}
泛型组件:
// 一个可以渲染任意列表的组件
interface ListProps<T> {
items: T[];
renderItem: (item: T, index: number) => React.ReactNode;
keyExtractor: (item: T) => string | number;
emptyText?: string;
}
function List<T>({ items, renderItem, keyExtractor, emptyText = '暂无数据' }: ListProps<T>) {
if (items.length === 0) {
return <p>{emptyText}</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor(item)}>
{renderItem(item, index)}
</li>
))}
</ul>
);
}
// 使用泛型组件
<List
items={users}
keyExtractor={user => user.id}
renderItem={user => <span>{user.name}</span>}
/>
7. 在 Vite 项目中配置 TypeScript
# 创建 React + TypeScript 项目
npm create vite@latest my-app -- --template react-ts
tsconfig.json 关键配置:
{
"compilerOptions": {
"target": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true, // 开启严格模式(推荐)
"noUnusedLocals": true, // 未使用变量报错
"noUnusedParameters": true, // 未使用参数报错
"jsx": "react-jsx",
"paths": {
"@/*": ["./src/*"] // 路径别名:@ 指向 src
}
}
}
🤖 AI协作实战:AI 帮你写类型定义 + 解释复杂泛型
场景1:从 API 响应自动生成类型
我给 AI 的 Prompt:
这是我们后端返回的用户 API 响应 JSON,请帮我生成对应的 TypeScript 类型定义:
{
"code": 0,
"message": "success",
"data": {
"user": {
"id": "u_123",
"username": "zhangsan",
"email": "zs@example.com",
"profile": {
"avatar": "https://cdn.example.com/avatar.jpg",
"bio": "前端工程师",
"website": null,
"followersCount": 1024,
"followingCount": 88
},
"roles": ["user", "creator"],
"createdAt": "2024-01-15T08:00:00Z",
"lastLoginAt": "2024-06-01T10:30:00Z"
}
}
}
AI 的输出:
interface UserProfile {
avatar: string;
bio: string;
website: string | null;
followersCount: number;
followingCount: number;
}
type UserRole = 'user' | 'creator' | 'admin';
interface User {
id: string;
username: string;
email: string;
profile: UserProfile;
roles: UserRole[];
createdAt: string; // ISO 8601 日期字符串
lastLoginAt: string;
}
interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
interface GetUserResponse {
user: User;
}
// 使用方式:
type UserApiResponse = ApiResponse<GetUserResponse>;
场景2:解释复杂泛型
当我遇到看不懂的第三方库类型时:
请解释这个 TypeScript 类型是什么意思,用中文简单说明每一部分:
type DeepPartial<T> = T extends object
? { [P in keyof T]?: DeepPartial<T[P]> }
: T;
AI 的解释:这是一个递归泛型,把对象的所有属性(包括嵌套属性)都变成可选的。T extends object 判断 T 是否是对象类型,如果是,就用 keyof T 遍历所有键并加上 ?(可选),同时对每个值递归应用 DeepPartial;如果不是对象(比如 string、number),就直接返回原类型。
💻 动手练习
简单: 给以下 JavaScript 函数添加完整的 TypeScript 类型注解:
function formatPrice(price, currency, locale) {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency: currency,
}).format(price);
}
中等: 定义一个完整的 Post 类型系统:
Post接口:id、title、content、author(User类型)、tags(字符串数组)、status(‘draft’ | ‘published’ | ‘archived’)、publishedAt(可选)CreatePostDTO:用Omit排除 id 和 publishedAt,用Partial处理 authorUpdatePostDTO:用Partial<Post>+Pick<Post, 'id'>确保 id 必传
挑战: 实现一个类型安全的 EventEmitter:
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void)emit<K extends keyof Events>(event: K, data: Events[K])- 使用泛型确保事件名和数据类型强相关,传错数据类型时编译报错
📌 本期要点
- TypeScript 的价值在于编译期发现错误,不是为了让代码更复杂
- 优先用
interface描述数据结构,type处理联合类型和复杂类型运算 - 泛型 = 类型参数,让一段代码适配多种类型,
Partial/Pick/Omit/Record是高频工具 - React 组件 Props 用 interface 定义,
useState初始值为 null/[] 时需要手动标注泛型 - 遇到看不懂的类型,让 AI 解释——复杂泛型语义正是 AI 的强项
🔗 下期预告
第17期:Vite 与工程化——现代前端项目是如何组织的?Vite 配置、路径别名、环境变量、代码规范……让你的项目结构从一开始就像专业团队的水平。
如果你没有苹果电脑,需要上传ios到APPStore可以访问以下网站
iPA上传工具 - IPA解析与AppStore提交
更多推荐
所有评论(0)