第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;如果不是对象(比如 stringnumber),就直接返回原类型。


💻 动手练习

简单: 给以下 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 处理 author
  • UpdatePostDTO:用 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])
  • 使用泛型确保事件名和数据类型强相关,传错数据类型时编译报错

📌 本期要点

  1. TypeScript 的价值在于编译期发现错误,不是为了让代码更复杂
  2. 优先用 interface 描述数据结构type 处理联合类型和复杂类型运算
  3. 泛型 = 类型参数,让一段代码适配多种类型,Partial/Pick/Omit/Record 是高频工具
  4. React 组件 Props 用 interface 定义useState 初始值为 null/[] 时需要手动标注泛型
  5. 遇到看不懂的类型,让 AI 解释——复杂泛型语义正是 AI 的强项

🔗 下期预告

第17期:Vite 与工程化——现代前端项目是如何组织的?Vite 配置、路径别名、环境变量、代码规范……让你的项目结构从一开始就像专业团队的水平。
如果你没有苹果电脑,需要上传ios到APPStore可以访问以下网站
iPA上传工具 - IPA解析与AppStore提交

更多推荐