全部文章

TypeScript 实用技巧

TypeScript 类型体操、工具类型、配置优化与常见模式速查

目录 17 节

TypeScript 实用技巧

这页适合已经会写基础 TypeScript、准备把类型系统真正变成“开发效率工具”的人。重点不是追求复杂类型体操,而是先把常用工具类型、类型收窄、严格配置和常见边界用顺。

推荐掌握顺序

建议按下面顺序学习和落地:

  1. 先掌握 PartialPickOmitRecord
  2. 再理解联合类型、类型收窄、自定义类型守卫
  3. 然后进入泛型与返回值推导
  4. 最后才去碰条件类型、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
  • 对外部接口返回值优先做运行时校验,不要只相信类型声明
  • 类型命名尽量直白,例如 UserDtoUserFormUserEntity

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 校验或显式判断。

延伸阅读

参考链接

阅读建议
  • - 先读标题和摘要,再结合目录决定从哪个章节开始精读。
  • - 看到具体命令、配置或步骤时,尽量在自己的环境里同步验证。
  • - 如果你只是快速查资料,可先看目录和相关文档,再决定是否深入全文。
适合谁看
  • - 希望把零散经验整理成长期可复用工作流的人
  • - 想先建立认知,再决定是否深入实践的人
  • - 希望阅读时顺手建立自己的操作清单或收藏体系的人
执行前检查
  • - 先浏览标题、摘要和目录,带着问题阅读会更高效
  • - 顺手记录真正对你有用的命令、链接和注意事项,避免重复搜索
  • - 如果页面里提到相关文档,尽量一起打开对照,效果通常更完整
同类内容
← 上一篇tmux 与 screen 终端复用