TypeScript 实用技巧
这页适合已经会写基础 TypeScript、准备把类型系统真正变成“开发效率工具”的人。重点不是追求复杂类型体操,而是先把常用工具类型、类型收窄、严格配置和常见边界用顺。
推荐掌握顺序
建议按下面顺序学习和落地:
- 先掌握
Partial、Pick、Omit、Record - 再理解联合类型、类型收窄、自定义类型守卫
- 然后进入泛型与返回值推导
- 最后才去碰条件类型、
infer、品牌类型等高级技巧
能解决 80% 工程问题的,通常还是前两层。
常用工具类型
// Partial — 所有属性可选
type UserUpdate = Partial<User>;
// Required — 所有属性必填
type RequiredUser = Required<User>;
// Pick — 选取部分属性
type UserPreview = Pick<User, "id" | "name">;
// Omit — 排除部分属性
type UserWithoutPassword = Omit<User, "password">;
// Record — 键值对类型
type StatusMap = Record<string, boolean>;
// ReturnType — 获取函数返回类型
type Result = ReturnType<typeof fetchUser>;
// Parameters — 获取函数参数类型
type Args = Parameters<typeof fetchUser>;
// Awaited — 解包 Promise
type Data = Awaited<ReturnType<typeof fetchUser>>;
类型收窄
// typeof
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string
}
return value.toFixed(2); // number
}
// in 操作符
interface Dog {
bark(): void;
}
interface Cat {
meow(): void;
}
function speak(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark();
} else {
animal.meow();
}
}
// 自定义类型守卫
function isString(value: unknown): value is string {
return typeof value === "string";
}
泛型模式
// 泛型函数
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
// 泛型约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// 条件类型
type IsString<T> = T extends string ? true : false;
// infer 推断
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
实用模式
严格的事件处理
type EventMap = {
click: { x: number; y: number };
change: { value: string };
submit: { data: FormData };
};
function on<K extends keyof EventMap>(
event: K,
handler: (payload: EventMap[K]) => void,
) {
// ...
}
on("click", ({ x, y }) => {}); // 类型安全
构建器模式
class QueryBuilder<T> {
where<K extends keyof T>(key: K, value: T[K]): this {
// ...
return this;
}
select<K extends keyof T>(...keys: K[]): Pick<T, K>[] {
// ...
}
}
品牌类型(防止混淆)
type UserId = string & { __brand: "UserId" };
type PostId = string & { __brand: "PostId" };
function getUser(id: UserId) {
/* ... */
}
function getPost(id: PostId) {
/* ... */
}
const userId = "abc" as UserId;
getUser(userId); // OK
// getPost(userId) // Error
tsconfig 推荐配置
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"isolatedModules": true
}
}
常见工程建议
- 打开
strict,让问题尽量在编辑阶段暴露 any能少用就少用,优先unknown- 对外部接口返回值优先做运行时校验,不要只相信类型声明
- 类型命名尽量直白,例如
UserDto、UserForm、UserEntity
TypeScript 解决的是“开发时的约束与提示”,不是运行时的真实验证。凡是来自接口、用户输入、环境变量的数据,都要意识到它们在运行时依然可能是脏的。
常用声明技巧
// 扩展全局类型
declare global {
interface Window {
__APP_VERSION__: string;
}
}
// 模块声明
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}
// 环境变量类型
interface ImportMetaEnv {
readonly VITE_API_URL: string;
}
常见问题
类型写得很复杂,但维护更难了
这通常说明类型已经开始服务于“炫技”而不是可维护性。优先让类型表达业务边界,而不是追求一眼看不懂的高级推导。
as 到处都是
大量 as 往往是在绕过编译器,而不是利用编译器。出现这种情况时,通常应该回头检查:
- 数据来源是不是没有校验
- 泛型和函数签名是不是设计不合理
- 类型是不是被拆得太散或太模糊
类型正确,但运行时还是报错
因为 TypeScript 不会自动帮你做运行时校验。接口响应、环境变量、表单输入、URL 参数,必要时都要配合 schema 校验或显式判断。
延伸阅读
参考链接
- TypeScript 官网 — 文档与 Playground
- TypeScript GitHub — 源码
- Type Challenges — 类型体操练习
- Matt Pocock — Total TypeScript 教程