16 - TypeScript 高级实践¶
学习时间: 5小时 难度级别: 高级 重要性: ⭐⭐⭐⭐⭐ 现代前端开发的类型安全基石
🎯 学习目标¶
完成本章后,你将能够: - 掌握 TypeScript 5.x 核心新特性并在项目中实际应用 - 深入理解高级类型系统(条件类型、模板字面量类型、递归类型等) - 实现生产级的类型体操工具类型 - 使用 Zod + tRPC 构建端到端类型安全的 API - 精通 React + TypeScript 最佳实践 - 优化 TypeScript 编译器配置以提升项目质量
1. TypeScript 5 核心特性¶
1.1 const 类型参数¶
TypeScript 5.0 引入了 const 类型参数,使泛型推断保留字面量类型,无需显式 as const。
// ❌ 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 装饰器,取代了实验性装饰器语法。
// === 类装饰器(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 在确保值符合类型约束的同时,保留最精确的推断类型。
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 重要特性¶
// === 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 用于在条件类型中提取(推断)类型。
// === 基本语法 ===
// 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)¶
模板字面量类型将字符串字面量类型与模板字符串语法结合,实现字符串级别的类型安全。
// === 基本用法 ===
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 子句实现键的重映射。
// === 基本映射类型 ===
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)¶
递归类型可以引用自身,用于处理嵌套数据结构。
// === 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 类型体操实战¶
实现常见的高级工具类型,这些在面试和实际项目中经常使用。
// === 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 类型。
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 实现了从后端到前端的零代码生成的完整类型安全。
// === 后端:定义 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 类型设计¶
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 类型)¶
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 类型安全¶
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 + 泛型组件¶
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 模式各选项详解¶
// 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 实现增量编译和模块隔离。
// === 根 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"]
}
# 全量构建(首次)
tsc --build
# 增量构建(只重建变更的包及其依赖者)
tsc --build --incremental
# 清理所有构建产物
tsc --build --clean
# 查看依赖图
tsc --build --listFiles
6. 面试题精选¶
面试题 1:说明 type 和 interface 的区别与适用场景¶
参考答案: - interface 支持声明合并(同名自动合并)、extends 继承,适合定义对象形状和公共 API - type 支持联合类型、交叉类型、映射类型、条件类型等,适合复杂类型运算 - 性能上 interface 在大型项目中略优(编译器对 interface 有优化缓存) - 最佳实践:公共 API 用 interface,类型运算用 type
面试题 2:实现 DeepReadonly<T>,递归地将所有属性设为只读¶
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:unknown 和 any 有什么区别?¶
| 特性 | any | unknown |
|---|---|---|
| 可赋值给任何类型 | ✅ | ❌(只能赋给 unknown / any) |
| 任何类型可赋值给它 | ✅ | ✅ |
| 访问属性 / 调用方法 | ✅(不安全) | ❌(需先收窄类型) |
| 类型安全 | 不安全 | 安全 |
面试题 5:如何实现类型安全的 EventEmitter?¶
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>¶
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 的项目组织机制,通过 composite 和 references 配置 - 支持增量编译:只重新编译变更的项目及其依赖者,大幅加速编译 - 支持模块边界:每个子项目只能访问显式声明的依赖 - 适用场景: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 管理大型项目