跳转至

16 - TypeScript 高级实践

TypeScript高级实践

学习时间: 5小时 难度级别: 高级 重要性: ⭐⭐⭐⭐⭐ 现代前端开发的类型安全基石


🎯 学习目标

完成本章后,你将能够: - 掌握 TypeScript 5.x 核心新特性并在项目中实际应用 - 深入理解高级类型系统(条件类型、模板字面量类型、递归类型等) - 实现生产级的类型体操工具类型 - 使用 Zod + tRPC 构建端到端类型安全的 API - 精通 React + TypeScript 最佳实践 - 优化 TypeScript 编译器配置以提升项目质量


1. TypeScript 5 核心特性

1.1 const 类型参数

TypeScript 5.0 引入了 const 类型参数,使泛型推断保留字面量类型,无需显式 as const

TypeScript
// ❌ TS4: 需要 as const 才能保留字面量类型
function getRoutes<T extends readonly string[]>(routes: T): T {
  return routes;
}
const routes = getRoutes(["home", "about", "contact"] as const);
// type: readonly ["home", "about", "contact"]

// ✅ TS5: const 类型参数自动推断为字面量类型
function getRoutes5<const T extends readonly string[]>(routes: T): T {
  return routes;
}
const routes5 = getRoutes5(["home", "about", "contact"]);
// type: readonly ["home", "about", "contact"] — 无需 as const

// 实际应用:类型安全的配置构建器
function defineConfig<const T extends {
  routes: Record<string, { path: string; auth?: boolean }>;
  plugins: readonly string[];
}>(config: T): T {
  return config;
}

const appConfig = defineConfig({
  routes: {
    home: { path: "/", auth: false },
    dashboard: { path: "/dashboard", auth: true },
  },
  plugins: ["analytics", "sentry"],
});
// appConfig.routes.home.path 的类型是 "/" 而不是 string
// appConfig.plugins 的类型是 readonly ["analytics", "sentry"]

1.2 装饰器(ECMAScript 标准装饰器)

TypeScript 5.0 正式支持 TC39 Stage 3 装饰器,取代了实验性装饰器语法。

TypeScript
// === 类装饰器(TC39 Stage 3 标准签名)===
function sealed(target: Function, context: ClassDecoratorContext) {
  Object.seal(target);
  Object.seal(target.prototype);
}

// === 方法装饰器 ===
function log(
  target: any,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`[LOG] ${methodName} called with:`, args);
    const result = target.apply(this, args);
    console.log(`[LOG] ${methodName} returned:`, result);
    return result;
  };
}

// === 自动绑定装饰器 ===
function bound(
  target: any,
  context: ClassMethodDecoratorContext
) {
  const methodName = context.name;
  context.addInitializer(function (this: any) {
    this[methodName] = this[methodName].bind(this);
  });
}

// === 自动访问器装饰器 ===
// 注意:accessor 字段使用 ClassAccessorDecoratorContext(非 ClassFieldDecoratorContext)
function range(min: number, max: number) {
  return function <T extends number>(
    target: ClassAccessorDecoratorTarget<any, T>,
    context: ClassAccessorDecoratorContext<any, T>
  ): ClassAccessorDecoratorResult<any, T> {
    return {
      // init 在初始化时验证
      init(value: T): T {
        if (value < min || value > max) {
          throw new RangeError(
            `${String(context.name)} must be between ${min} and ${max}`
          );
        }
        return value;
      },
      // set 在赋值时验证
      set(value: T) {
        if (value < min || value > max) {
          throw new RangeError(
            `${String(context.name)} must be between ${min} and ${max}`
          );
        }
        return value;
      },
    };
  };
}

// === 使用示例 ===
@sealed
class UserService {
  @range(0, 150)
  accessor age: number = 25;

  @log
  @bound
  getUser(id: string) {
    return { id, name: "Alice", age: this.age };
  }
}

const service = new UserService();
const getUser = service.getUser; // 已自动绑定
getUser("123"); // [LOG] getUser called with: ["123"]

1.3 satisfies 操作符

satisfies 在确保值符合类型约束的同时,保留最精确的推断类型。

TypeScript
type ColorMap = Record<string, [number, number, number] | string>;

// ❌ 使用类型注解:丢失字面量类型信息
const colors1: ColorMap = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
};
// colors1.green 的类型是 string | [number, number, number] — 不精确

// ✅ 使用 satisfies:保留精确类型
const colors2 = {
  red: [255, 0, 0],
  green: "#00ff00",
  blue: [0, 0, 255],
} satisfies ColorMap;
// colors2.green 的类型是 string — 精确!
// colors2.red 的类型是 [number, number, number] — 精确!

colors2.green.toUpperCase(); // ✅ 类型安全
colors2.red.map(v => v / 255); // ✅ 类型安全

// 实际应用:路由配置
type Route = { path: string; component: () => JSX.Element; layout?: string };
type AppRoutes = Record<string, Route>;

const routes = {
  home: { path: "/", component: HomePage },
  about: { path: "/about", component: AboutPage, layout: "main" },
  dashboard: { path: "/dashboard", component: DashboardPage, layout: "admin" },
} satisfies AppRoutes;

// routes.dashboard.layout 的类型是 string(而非 string | undefined)

1.4 其他 TS5 重要特性

TypeScript
// === 1. 枚举增强:所有枚举都可以是联合枚举 ===
enum LogLevel {
  Debug = "debug",
  Info = "info",
  Warn = "warn",
  Error = "error",
}
// TS5 中枚举成员都是类型
function handleLog(level: LogLevel.Error | LogLevel.Warn) { /* ... */ }

// === 2. switch(true) 的类型收窄 ===
function processValue(value: string | number | boolean) {
  switch (true) {
    case typeof value === "string":
      return value.toUpperCase(); // TS5 正确收窄为 string
    case typeof value === "number":
      return value.toFixed(2);    // 正确收窄为 number
    case typeof value === "boolean":
      return value ? "yes" : "no"; // 正确收窄为 boolean
  }
}

// === 3. 模块解析 bundler 模式 ===
// tsconfig.json
// { "compilerOptions": { "moduleResolution": "bundler" } }
// 专为现代打包器设计,支持 package.json exports 字段

2. 高级类型系统

2.1 条件类型(Conditional Types)+ infer 关键字

条件类型是 TypeScript 类型系统中的 "if-else",infer 用于在条件类型中提取(推断)类型。

TypeScript
// === 基本语法 ===
// T extends U ? X : Y

// 基础示例
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>;       // false

// === infer 关键字:类型推断 ===
// 提取函数返回值类型
// infer R在条件类型中声明“占位类型变量”:若T匹配函数签名则R自动推断为返回值类型,否则返回never
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (x: number) => string;
type R = MyReturnType<Fn>; // string

// 提取函数参数类型
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;
type Params = MyParameters<(a: string, b: number) => void>; // [string, number]

// 提取 Promise 内部类型
type UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;
type P1 = UnwrapPromise<Promise<Promise<string>>>; // string

// 提取数组元素类型
type ElementOf<T> = T extends (infer E)[] ? E : never;
type El = ElementOf<string[]>; // string

// === 高级 infer 用法 ===
// 提取字符串中的模式
type ExtractRouteParams<T extends string> =
  T extends `${string}:${infer Param}/${infer Rest}`
    ? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
    : T extends `${string}:${infer Param}`
    ? { [K in Param]: string }
    : {};

type RouteParams = ExtractRouteParams<"/users/:userId/posts/:postId">;
// { userId: string; postId: string }

// === 分布式条件类型 ===
// 当 T 是联合类型时,条件类型会分配到每个成员
type ToArray<T> = T extends any ? T[] : never;
type Distributed = ToArray<string | number>; // string[] | number[]

// 阻止分布式行为
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type NonDist = ToArrayNonDist<string | number>; // (string | number)[]

2.2 模板字面量类型(Template Literal Types)

模板字面量类型将字符串字面量类型与模板字符串语法结合,实现字符串级别的类型安全。

TypeScript
// === 基本用法 ===
type EventName = `on${Capitalize<"click" | "focus" | "blur">}`;
// "onClick" | "onFocus" | "onBlur"

// 内置字符串操作类型
type Upper = Uppercase<"hello">;     // "HELLO"
type Lower = Lowercase<"HELLO">;     // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;  // "hello"

// === CSS 属性类型 ===
type CSSValue = `${number}${"px" | "em" | "rem" | "vh" | "vw" | "%"}`;
const width: CSSValue = "100px";  // ✅
const height: CSSValue = "50vh";  // ✅
// const bad: CSSValue = "auto";  // ❌ 编译错误

// === 类型安全的事件系统 ===
type EventMap = {
  click: { x: number; y: number };
  focus: { target: HTMLElement };
  keydown: { key: string; code: string };
};

type EventHandler<T extends keyof EventMap> = (event: EventMap[T]) => void;

// 构建严格的事件监听器类型
type StrictEventListener = {
  [K in keyof EventMap as `on${Capitalize<K & string>}`]: EventHandler<K>;
};
// {
//   onClick: (event: { x: number; y: number }) => void;
//   onFocus: (event: { target: HTMLElement }) => void;
//   onKeydown: (event: { key: string; code: string }) => void;
// }

// === 类型安全的 API 路由 ===
type APIRoute = `/api/${"v1" | "v2"}/${"users" | "posts" | "comments"}`;
// "/api/v1/users" | "/api/v1/posts" | ... 共6种组合

// 解析路径参数
type ParsePath<T extends string> =
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ParsePath<`/${Rest}`>
    : T extends `${infer _Start}:${infer Param}`
    ? Param
    : never;

type Params = ParsePath<"/api/users/:userId/posts/:postId">;
// "userId" | "postId"

2.3 映射类型(Mapped Types)+ 重映射

映射类型通过遍历已有类型的键来创建新类型,as 子句实现键的重映射。

TypeScript
// === 基本映射类型 ===
type Optional<T> = { [K in keyof T]?: T[K] };
type Readonly2<T> = { readonly [K in keyof T]: T[K] };
type Nullable<T> = { [K in keyof T]: T[K] | null };

// === 键重映射(Key Remapping) ===
// 添加前缀
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${Capitalize<K & string>}`]: T[K];
};

interface User {
  name: string;
  age: number;
  email: string;
}

type GetterUser = Prefixed<User, "get">;
// { getName: string; getAge: number; getEmail: string }

// === Getter/Setter 生成 ===
type Getters<T> = {
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type Setters<T> = {
  [K in keyof T as `set${Capitalize<K & string>}`]: (value: T[K]) => void;
};
type WithAccessors<T> = T & Getters<T> & Setters<T>;

// === 过滤键 ===
// 只保留值为函数的属性
type MethodsOf<T> = {
  [K in keyof T as T[K] extends Function ? K : never]: T[K];
};

class MyClass {
  name = "test";
  age = 25;
  greet() { return "hello"; }
  calculate(x: number) { return x * 2; }
}

type Methods = MethodsOf<MyClass>;
// { greet: () => string; calculate: (x: number) => number }

// === 移除特定类型的键 ===
type OmitByType<T, U> = {
  [K in keyof T as T[K] extends U ? never : K]: T[K];
};

type WithoutStrings = OmitByType<User, string>;
// { age: number }

2.4 递归类型(Recursive Types)

递归类型可以引用自身,用于处理嵌套数据结构。

TypeScript
// === JSON 类型 ===
type JSONValue =
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// === 深度嵌套的树形结构 ===
type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};

const fileTree: TreeNode<string> = {
  value: "src",
  children: [
    { value: "index.ts", children: [] },
    {
      value: "utils",
      children: [
        { value: "helpers.ts", children: [] },
      ],
    },
  ],
};

// === 递归展平元组 ===
// 递归展平:infer First/Rest解构首元素和剩余,若First是数组则递归展平后拼接,否则保留First并递归处理Rest
type Flatten<T extends any[]> =
  T extends [infer First, ...infer Rest]
    ? First extends any[]
      ? [...Flatten<First>, ...Flatten<Rest>]
      : [First, ...Flatten<Rest>]
    : [];

type Flat = Flatten<[1, [2, 3], [4, [5, 6]]]>;
// [1, 2, 3, 4, 5, 6]

// === 递归类型实用工具 ===
// 将嵌套对象的所有键路径提取为联合类型(见下节 PathOf)

2.5 类型体操实战

实现常见的高级工具类型,这些在面试和实际项目中经常使用。

TypeScript
// === 1. DeepReadonly ===
type DeepReadonly<T> = T extends Function
  ? T
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

interface Config {
  db: { host: string; port: number; options: { ssl: boolean } };
  cache: { ttl: number };
}
type ReadonlyConfig = DeepReadonly<Config>;
// 所有嵌套属性都变为 readonly

// === 2. DeepPartial ===
type DeepPartial<T> = T extends Function
  ? T
  : T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// 允许部分更新深度嵌套的配置
function updateConfig(patch: DeepPartial<Config>) { /* ... */ }
updateConfig({ db: { options: { ssl: true } } }); // ✅ 只更新 ssl

// === 3. PathOf — 提取对象所有嵌套路径 ===
type PathOf<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]:
        | `${Prefix}${K}`
        | PathOf<T[K], `${Prefix}${K}.`>;
    }[keyof T & string]
  : never;

type ConfigPaths = PathOf<Config>;
// "db" | "db.host" | "db.port" | "db.options" | "db.options.ssl" | "cache" | "cache.ttl"

// 类型安全的 get 函数
function get<T, P extends PathOf<T>>(obj: T, path: P): unknown {
  return path.split(".").reduce((acc: any, key) => acc?.[key], obj);
}

// === 4. DeepRequired ===
type DeepRequired<T> = T extends Function
  ? T
  : T extends object
  ? { [K in keyof T]-?: DeepRequired<T[K]> }
  : T;

// === 5. TupleToUnion ===
type TupleToUnion<T extends readonly any[]> = T[number];
type U = TupleToUnion<[string, number, boolean]>; // string | number | boolean

// === 6. UnionToIntersection ===
type UnionToIntersection<U> =
  (U extends any ? (x: U) => void : never) extends (x: infer I) => void
    ? I
    : never;

type Inter = UnionToIntersection<{ a: 1 } | { b: 2 }>;
// { a: 1 } & { b: 2 }

// === 7. PickByValue ===
type PickByValue<T, V> = {
  [K in keyof T as T[K] extends V ? K : never]: T[K];
};

interface Mixed {
  name: string;
  age: number;
  active: boolean;
  email: string;
}
type StringProps = PickByValue<Mixed, string>;
// { name: string; email: string }

// === 8. MutableKeys / ReadonlyKeys ===
type MutableKeys<T> = {
  [K in keyof T]-?: IfEquals<
    { [Q in K]: T[K] },
    { -readonly [Q in K]: T[K] },
    K
  >;
}[keyof T];

type IfEquals<X, Y, A = X, B = never> =
  (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
    ? A
    : B;

3. 类型安全的 API 设计

3.1 Zod Schema 验证

Zod 是一个 TypeScript-first 的 schema 验证库,可以从 schema 自动推导 TypeScript 类型。

TypeScript
import { z } from "zod";

// === 定义 Schema ===
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  age: z.number().int().min(0).max(150).optional(),
  role: z.enum(["admin", "user", "moderator"]),
  preferences: z.object({
    theme: z.enum(["light", "dark"]).default("light"),
    notifications: z.boolean().default(true),
    language: z.string().default("zh-CN"),
  }).optional(),
  tags: z.array(z.string()).max(10).default([]),
  createdAt: z.coerce.date(),
});

// 自动推导类型 — 无需手写 interface
type User = z.infer<typeof UserSchema>;
// {
//   id: string;
//   name: string;
//   email: string;
//   age?: number;
//   role: "admin" | "user" | "moderator";
//   preferences?: { theme: "light" | "dark"; notifications: boolean; language: string };
//   tags: string[];
//   createdAt: Date;
// }

// === 运行时验证 ===
function createUser(input: unknown): User {
  const result = UserSchema.safeParse(input);
  if (!result.success) {
    // result.error 包含详细的验证错误信息
    const formatted = result.error.format();
    throw new Error(`Validation failed: ${JSON.stringify(formatted)}`);
  }
  return result.data; // 类型安全的 User
}

// === Schema 组合与变换 ===
const CreateUserInput = UserSchema.omit({ id: true, createdAt: true });
const UpdateUserInput = UserSchema.partial().omit({ id: true });
const UserResponse = UserSchema.extend({
  postCount: z.number(),
  lastLogin: z.date().nullable(),
});

type CreateUserInput = z.infer<typeof CreateUserInput>;
type UpdateUserInput = z.infer<typeof UpdateUserInput>;

// === 自定义验证 ===
const PasswordSchema = z
  .string()
  .min(8, "密码至少8个字符")
  .regex(/[A-Z]/, "需要至少一个大写字母")
  .regex(/[0-9]/, "需要至少一个数字")
  .regex(/[^A-Za-z0-9]/, "需要至少一个特殊字符");

const RegisterSchema = z
  .object({
    email: z.string().email(),
    password: PasswordSchema,
    confirmPassword: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "两次密码不一致",
    path: ["confirmPassword"],
  });

3.2 tRPC 端到端类型安全

tRPC 实现了从后端到前端的零代码生成的完整类型安全。

TypeScript
// === 后端:定义 Router ===
// server/trpc.ts
import { initTRPC, TRPCError } from "@trpc/server";
import { z } from "zod";

const t = initTRPC.context<{ userId?: string }>().create();

const isAuthed = t.middleware(({ ctx, next }) => {
  if (!ctx.userId) {
    throw new TRPCError({ code: "UNAUTHORIZED" });
  }
  return next({ ctx: { userId: ctx.userId } });
});

const publicProcedure = t.procedure;
const protectedProcedure = t.procedure.use(isAuthed);

// server/routers/user.ts
export const userRouter = t.router({
  getById: publicProcedure
    .input(z.object({ id: z.string().uuid() }))
    .query(async ({ input }) => {
      const user = await db.user.findUnique({ where: { id: input.id } });
      if (!user) throw new TRPCError({ code: "NOT_FOUND" });
      return user;
    }),

  list: publicProcedure
    .input(z.object({
      page: z.number().int().min(1).default(1),
      pageSize: z.number().int().min(1).max(100).default(20),
      role: z.enum(["admin", "user"]).optional(),
    }))
    .query(async ({ input }) => {
      return db.user.findMany({
        skip: (input.page - 1) * input.pageSize,
        take: input.pageSize,
        where: input.role ? { role: input.role } : undefined,
      });
    }),

  update: protectedProcedure
    .input(z.object({
      name: z.string().min(2).optional(),
      email: z.string().email().optional(),
    }))
    .mutation(async ({ input, ctx }) => {
      return db.user.update({
        where: { id: ctx.userId },
        data: input,
      });
    }),
});

export type AppRouter = typeof appRouter;

// === 前端:完全类型安全的调用 ===
// client/trpc.ts
import { createTRPCReact } from "@trpc/react-query";
import type { AppRouter } from "../server/routers";

export const trpc = createTRPCReact<AppRouter>();

// React 组件中使用
function UserProfile({ userId }: { userId: string }) {
  // ✅ input 类型自动推断:{ id: string }
  // ✅ data 类型自动推断为 User
  const { data: user, isLoading, error } = trpc.user.getById.useQuery({ id: userId });

  // ✅ mutate 的参数类型自动推断
  const updateMutation = trpc.user.update.useMutation({
    onSuccess: () => { /* 更新成功 */ },
  });

  if (isLoading) return <Loading />;
  if (error) return <Error message={error.message} />;

  return (
    <div>
      <h1>{user.name}</h1>  {/*  user.name 类型安全 */}
      <button onClick={() => updateMutation.mutate({ name: "New Name" })}>
        更新
      </button>
    </div>
  );
}

4. React + TypeScript 最佳实践

4.1 组件 Props 类型设计

TypeScript
import React, { ReactNode, ComponentPropsWithoutRef, ComponentPropsWithRef } from "react";

// === 基础 Props 设计 ===
// 1. 使用 interface + 明确的可选属性
interface ButtonProps {
  variant: "primary" | "secondary" | "danger";
  size?: "sm" | "md" | "lg";
  disabled?: boolean;
  loading?: boolean;
  children: ReactNode;
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
}

// 2. 扩展原生 HTML 属性
interface InputProps extends ComponentPropsWithoutRef<"input"> {
  label: string;
  error?: string;
  helperText?: string;
}

// 3. 多态组件(as prop)
type PolymorphicProps<E extends React.ElementType, P = {}> = P &
  Omit<ComponentPropsWithoutRef<E>, keyof P> & {
    as?: E;
  };

function Box<E extends React.ElementType = "div">({
  as,
  children,
  ...props
}: PolymorphicProps<E, { children?: ReactNode }>) {
  const Component = as || "div";
  return <Component {...props}>{children}</Component>;
}

// 使用
<Box as="a" href="/home">Link</Box>       //  a标签的属性
<Box as="button" onClick={() => {}}>Btn</Box> //  button的属性

// === 4. 辨识联合类型(Discriminated Union) ===
type AlertProps =
  | { type: "success"; onClose: () => void }
  | { type: "error"; onRetry: () => void; errorCode: number }
  | { type: "warning"; onDismiss: () => void };

function Alert(props: AlertProps) {
  switch (props.type) {
    case "success":
      return <div onClick={props.onClose}> Success</div>;
    case "error":
      return <div onClick={props.onRetry}> Error {props.errorCode}</div>;
    case "warning":
      return <div onClick={props.onDismiss}>⚠️ Warning</div>;
  }
}

4.2 Hook 类型(泛型 Hook、useReducer 类型)

TypeScript
import { useState, useReducer, useCallback, useEffect, useRef } from "react";

// === 泛型 Hook:useFetch ===
interface FetchState<T> {
  data: T | null;
  loading: boolean;
  error: Error | null;
}

function useFetch<T>(url: string, options?: RequestInit): FetchState<T> {
  const [state, setState] = useState<FetchState<T>>({
    data: null,
    loading: true,
    error: null,
  });

  useEffect(() => {
    const controller = new AbortController();
    setState({ data: null, loading: true, error: null });

    fetch(url, { ...options, signal: controller.signal })
      .then((res) => {
        if (!res.ok) throw new Error(`HTTP ${res.status}`);
        return res.json() as Promise<T>;
      })
      .then((data) => setState({ data, loading: false, error: null }))
      .catch((error) => {
        if (error.name !== "AbortError") {
          setState({ data: null, loading: false, error });
        }
      });

    return () => controller.abort();
  }, [url]);

  return state;
}

// 使用:返回值类型自动推断
const { data, loading } = useFetch<User[]>("/api/users");
// data 类型为 User[] | null

// === 类型安全的 useReducer ===
// 定义 State 和 Action 类型
interface TodoState {
  todos: { id: string; text: string; done: boolean }[];
  filter: "all" | "active" | "completed";
}

type TodoAction =
  | { type: "ADD_TODO"; payload: { text: string } }
  | { type: "TOGGLE_TODO"; payload: { id: string } }
  | { type: "DELETE_TODO"; payload: { id: string } }
  | { type: "SET_FILTER"; payload: { filter: TodoState["filter"] } };

function todoReducer(state: TodoState, action: TodoAction): TodoState {
  switch (action.type) {
    case "ADD_TODO":
      return {
        ...state,
        todos: [
          ...state.todos,
          { id: crypto.randomUUID(), text: action.payload.text, done: false },
        ],
      };
    case "TOGGLE_TODO":
      return {
        ...state,
        todos: state.todos.map((t) =>
          t.id === action.payload.id ? { ...t, done: !t.done } : t
        ),
      };
    case "DELETE_TODO":
      return {
        ...state,
        todos: state.todos.filter((t) => t.id !== action.payload.id),
      };
    case "SET_FILTER":
      return { ...state, filter: action.payload.filter };
  }
}

function TodoApp() {
  const [state, dispatch] = useReducer(todoReducer, {
    todos: [],
    filter: "all",
  });

  // ✅ dispatch 的参数类型完全安全
  dispatch({ type: "ADD_TODO", payload: { text: "学习TS" } });
  // ❌ dispatch({ type: "ADD_TODO", payload: { id: "1" } }); // 编译错误
}

// === 泛型 useLocalStorage ===
function useLocalStorage<T>(
  key: string,
  initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] {
  const [storedValue, setStoredValue] = useState<T>(() => {
    try {  // try/catch捕获异常
      const item = localStorage.getItem(key);
      return item ? (JSON.parse(item) as T) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = useCallback(
    (value: T | ((prev: T) => T)) => {
      setStoredValue((prev) => {
        const nextValue = value instanceof Function ? value(prev) : value;
        localStorage.setItem(key, JSON.stringify(nextValue));
        return nextValue;
      });
    },
    [key]
  );

  return [storedValue, setValue];
}

4.3 Context 类型安全

TypeScript
import { createContext, useContext, ReactNode, useReducer } from "react";

// === 解决 Context 默认值问题 ===
// 方法1:使用 null + 非空断言 Hook
interface AuthContextType {
  user: User | null;
  login: (email: string, password: string) => Promise<void>;  // Promise异步操作容器:pending→fulfilled/rejected
  logout: () => void;
  isAuthenticated: boolean;
}

const AuthContext = createContext<AuthContextType | null>(null);

// 自定义 Hook 封装 null 检查
function useAuth(): AuthContextType {
  const context = useContext(AuthContext);
  if (context === null) {
    throw new Error("useAuth must be used within an AuthProvider");
  }
  return context;
}

function AuthProvider({ children }: { children: ReactNode }) {
  const [user, setUser] = useState<User | null>(null);

  const login = async (email: string, password: string) => {  // async定义异步函数;await等待Promise完成
    const res = await fetch("/api/login", {  // await等待异步操作完成
      method: "POST",
      body: JSON.stringify({ email, password }),
    });
    const user = await res.json();
    setUser(user);
  };

  const logout = () => setUser(null);

  return (
    <AuthContext.Provider
      value={{ user, login, logout, isAuthenticated: !!user }}
    >
      {children}
    </AuthContext.Provider>
  );
}

// 使用
function Dashboard() {
  const { user, isAuthenticated, logout } = useAuth(); // ✅ 类型完全安全
  if (!isAuthenticated) return <Redirect to="/login" />;
  return <div>Welcome, {user!.name}</div>;
}

// === 方法2:泛型 Context 工厂 ===
function createSafeContext<T>(displayName: string) {
  const Context = createContext<T | undefined>(undefined);
  Context.displayName = displayName;

  function useContextSafe(): T {
    const ctx = useContext(Context);
    if (ctx === undefined) {
      throw new Error(`use${displayName} must be used within ${displayName}Provider`);
    }
    return ctx;
  }

  return [Context.Provider, useContextSafe] as const;
}

// 使用工厂
const [ThemeProvider, useTheme] = createSafeContext<{
  theme: "light" | "dark";
  toggle: () => void;
}>("Theme");

4.4 forwardRef + 泛型组件

TypeScript
import React, { forwardRef, useImperativeHandle, useRef } from "react";

// === forwardRef 的类型 ===
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {  // interface定义类型契约
  label: string;
  error?: string;
}

const Input = forwardRef<HTMLInputElement, InputProps>(
  ({ label, error, ...props }, ref) => (
    <div>
      <label>{label}</label>
      <input ref={ref} {...props} />
      {error && <span className="error">{error}</span>}
    </div>
  )
);
Input.displayName = "Input";

// === useImperativeHandle 暴露方法 ===
interface ModalHandle {
  open: () => void;
  close: () => void;
  toggle: () => void;
}

interface ModalProps {
  title: string;
  children: ReactNode;
}

const Modal = forwardRef<ModalHandle, ModalProps>(({ title, children }, ref) => {
  const [isOpen, setIsOpen] = useState(false);  // 解构赋值:从对象/数组提取值

  useImperativeHandle(ref, () => ({
    open: () => setIsOpen(true),
    close: () => setIsOpen(false),
    toggle: () => setIsOpen((prev) => !prev),
  }));

  if (!isOpen) return null;
  return (
    <div className="modal">
      <h2>{title}</h2>
      {children}
    </div>
  );
});

// 父组件使用
function App() {
  const modalRef = useRef<ModalHandle>(null);
  return (
    <>
      <button onClick={() => modalRef.current?.open()}>打开</button>
      <Modal ref={modalRef} title="提示">
        <p>内容</p>
      </Modal>
    </>
  );
}

// === 泛型组件 ===
// React.forwardRef 不直接支持泛型,需要类型断言
interface SelectProps<T> {
  options: T[];
  value: T | null;
  onChange: (value: T) => void;
  getLabel: (item: T) => string;
  getValue: (item: T) => string | number;
}

// 方法:使用函数声明(不用 forwardRef)
function GenericSelect<T>({ options, value, onChange, getLabel, getValue }: SelectProps<T>) {
  return (
    <select
      value={value ? String(getValue(value)) : ""}
      onChange={(e) => {
        const selected = options.find(
          (opt) => String(getValue(opt)) === e.target.value
        );
        if (selected) onChange(selected);
      }}
    >
      <option value="">请选择</option>
      {options.map((opt) => (
        <option key={String(getValue(opt))} value={String(getValue(opt))}>
          {getLabel(opt)}
        </option>
      ))}
    </select>
  );
}

// 使用:T 自动推断为 User
<GenericSelect
  options={users}
  value={selectedUser}
  onChange={setSelectedUser}
  getLabel={(u) => u.name}
  getValue={(u) => u.id}
/>

5. TypeScript 编译器配置优化

5.1 strict 模式各选项详解

Text Only
// tsconfig.json
{
  "compilerOptions": {
    // === strict: true 等价于以下所有选项 ===
    "strict": true,

    // 1. strictNullChecks: 区分 null/undefined 和其他类型
    // string 不再隐式包含 null | undefined
    "strictNullChecks": true,

    // 2. strictFunctionTypes: 函数参数类型检查(逆变)
    // 禁止不安全的函数类型赋值
    "strictFunctionTypes": true,

    // 3. strictBindCallApply: bind/call/apply 的参数类型检查
    "strictBindCallApply": true,

    // 4. strictPropertyInitialization: 类属性必须在构造函数中初始化
    "strictPropertyInitialization": true,

    // 5. noImplicitAny: 禁止隐式 any
    "noImplicitAny": true,

    // 6. noImplicitThis: 禁止隐式 this: any
    "noImplicitThis": true,

    // 7. alwaysStrict: 每个文件都加 "use strict"
    "alwaysStrict": true,

    // 8. useUnknownInCatchVariables: catch 变量类型为 unknown 而非 any
    "useUnknownInCatchVariables": true,

    // === 额外建议开启 ===
    "noUncheckedIndexedAccess": true,    // 索引访问返回 T | undefined
    "exactOptionalPropertyTypes": true,  // 区分 undefined 和缺失属性
    "noImplicitReturns": true,           // 确保所有分支都有返回值
    "noFallthroughCasesInSwitch": true,  // 禁止 switch 穿透
    "noImplicitOverride": true,          // 子类重写方法必须加 override
    "forceConsistentCasingInFileNames": true, // 文件名大小写一致

    // === 模块与输出 ===
    "module": "ESNext",
    "moduleResolution": "bundler",       // TS5: 专为打包器优化
    "target": "ES2022",
    "lib": ["ES2023", "DOM", "DOM.Iterable"],
    "isolatedModules": true,             // 确保每个文件可独立编译
    "verbatimModuleSyntax": true,        // TS5: 保持 import/export 语法
    "skipLibCheck": true,                // 跳过 .d.ts 检查(加速编译)
  }
}

5.2 Project References(项目引用)

大型 monorepo 项目通过 Project References 实现增量编译和模块隔离。

Text Only
// === 根 tsconfig.json ===
{
  "files": [],
  "references": [
    { "path": "./packages/shared" },
    { "path": "./packages/server" },
    { "path": "./packages/client" }
  ]
}

// === packages/shared/tsconfig.json ===
{
  "compilerOptions": {
    "composite": true,            // 启用项目引用
    "declaration": true,          // 生成 .d.ts
    "declarationMap": true,       // 支持 Go to Definition
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"]
}

// === packages/server/tsconfig.json ===
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "references": [
    { "path": "../shared" }       // 依赖 shared 包
  ],
  "include": ["src"]
}

// === packages/client/tsconfig.json ===
{
  "compilerOptions": {
    "composite": true,
    "outDir": "./dist",
    "rootDir": "./src",
    "jsx": "react-jsx"
  },
  "references": [
    { "path": "../shared" }
  ],
  "include": ["src"]
}
Bash
# 全量构建(首次)
tsc --build

# 增量构建(只重建变更的包及其依赖者)
tsc --build --incremental

# 清理所有构建产物
tsc --build --clean

# 查看依赖图
tsc --build --listFiles

6. 面试题精选

面试题 1:说明 typeinterface 的区别与适用场景

参考答案: - interface 支持声明合并(同名自动合并)、extends 继承,适合定义对象形状和公共 API - type 支持联合类型、交叉类型、映射类型、条件类型等,适合复杂类型运算 - 性能上 interface 在大型项目中略优(编译器对 interface 有优化缓存) - 最佳实践:公共 API 用 interface,类型运算用 type

面试题 2:实现 DeepReadonly<T>,递归地将所有属性设为只读

TypeScript
type DeepReadonly<T> = T extends Function  // 泛型<T>:类型参数化
  ? T
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

面试题 3:解释 TypeScript 中的协变与逆变

参考答案: - 协变(Covariant):子类型可以赋值给父类型(数组、返回值类型) - 逆变(Contravariant):父类型可以赋值给子类型(函数参数在 strict 模式下) - 双变(Bivariant):非 strict 模式下函数参数既协变又逆变 - strictFunctionTypes: true 启用函数参数的逆变检查,避免不安全赋值

面试题 4:unknownany 有什么区别?

特性 any unknown
可赋值给任何类型 ❌(只能赋给 unknown / any
任何类型可赋值给它
访问属性 / 调用方法 ✅(不安全) ❌(需先收窄类型)
类型安全 不安全 安全

面试题 5:如何实现类型安全的 EventEmitter

TypeScript
type EventMap = Record<string, any[]>;

class TypedEventEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: Array<(...args: Events[K]) => void>;
  } = {};

  on<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void) {
    (this.listeners[event] ??= []).push(listener);  // ??空值合并:左侧为null/undefined时使用右侧默认值
    return () => this.off(event, listener);
  }

  off<K extends keyof Events>(event: K, listener: (...args: Events[K]) => void) {
    this.listeners[event] = this.listeners[event]?.filter((l) => l !== listener);  // map转换每个元素;filter筛选;reduce累积
  }

  emit<K extends keyof Events>(event: K, ...args: Events[K]) {
    this.listeners[event]?.forEach((l) => l(...args));  // ?.可选链:对象为null/undefined时安全返回undefined
  }
}

// 使用
const emitter = new TypedEventEmitter<{  // const不可重新赋值;let块级作用域变量
  login: [user: User];
  error: [code: number, message: string];
  logout: [];
}>();

emitter.on("login", (user) => console.log(user.name)); // ✅ user: User
emitter.on("error", (code, msg) => console.log(code, msg)); // ✅
// emitter.emit("login", "wrong"); // ❌ 编译错误

面试题 6:解释 infer 关键字并实现 Parameters<T>

TypeScript
type MyParameters<T extends (...args: any) => any> =  // 箭头函数:简洁的函数语法
  T extends (...args: infer P) => any ? P : never;  // ...展开运算符:展开数组/对象

// infer 在条件类型中声明一个待推断的类型变量
// 编译器会根据 T 的实际类型自动推断 P 的具体类型

面试题 7:satisfies 与类型注解的区别是什么?

参考答案: - 类型注解 const x: Type = value 会将 x 的类型"拓宽"为 Type - satisfies验证值符合约束的同时,保留值的最窄推断类型 - 使用场景:需要同时做类型检查和保留字面量精度时(如配置对象、路由表)

面试题 8:什么是 Project References?在什么场景下使用?

参考答案: - Project References 是 TypeScript 的项目组织机制,通过 compositereferences 配置 - 支持增量编译:只重新编译变更的项目及其依赖者,大幅加速编译 - 支持模块边界:每个子项目只能访问显式声明的依赖 - 适用场景:monorepo、大型项目拆分、前后端共享类型


7. 检查清单

TypeScript 5 特性

  • 理解并使用 const 类型参数自动推断字面量类型
  • 使用标准装饰器(类/方法/字段/accessor 装饰器)
  • 使用 satisfies 操作符在保留推断类型的同时做类型检查
  • 配置 moduleResolution: "bundler" 以适配现代打包器

高级类型系统

  • 掌握条件类型 + infer 的各种推断场景
  • 使用模板字面量类型构建类型安全的字符串操作
  • 使用映射类型 + as 重映射实现类型转换
  • 理解并实现递归类型
  • 能独立实现 DeepReadonly / DeepPartial / PathOf 等工具类型

API 类型安全

  • 使用 Zod 定义可复用的 schema 并自动推导类型
  • 理解 tRPC 的端到端类型安全原理
  • 能设计类型安全的 REST API / RPC 接口

React + TypeScript

  • 合理设计 Props 类型(辨识联合、多态组件)
  • 编写类型安全的自定义 Hook(泛型、useReducer)
  • 使用 Context 工厂模式避免 null 检查
  • 理解 forwardRef 和泛型组件的类型限制

编译器配置

  • 理解 strict 模式下各选项的作用
  • 根据项目需求配置额外的类型检查选项
  • 使用 Project References 管理大型项目

上一章: 15-前端面试准备 返回目录: README