TypeScript 工程化实战:从类型体操到架构安全

系统性 TypeScript 工程化指南:tsconfig 严格模式配置、高级类型体操(条件类型、映射类型、模板字面量、infer)、类型守卫与 Narrowing、泛型约束与协变逆变、类型安全边界(noUncheckedIndexedAccess、exactOptionalPropertyTypes)、monorepo 类型共享、API 类型自动同步(OpenAPI → TS)、品牌类型(Branded Types)与名义类型系统。附 Vue/React/Node.js 实战配置。

TypeScript 的价值不在「有类型」,而在「类型即文档、类型即约束、类型即重构安全网」。 一个配置得当的 TS 项目能让 IDE 自动补全成为你的「结对程序员」,让重命名变量不再恐惧,让 API 变更的破坏面一望可知。


一、tsconfig 严格模式配置

1.1 企业级推荐配置

{
  "compilerOptions": {
    // 目标与模块
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],

    // 严格模式(全部开启)
    "strict": true,                          // 启用所有严格类型检查
    "noImplicitAny": true,                   // 禁止隐式 any
    "strictNullChecks": true,                // null/undefined 严格检查
    "strictFunctionTypes": true,             // 函数参数双向协变检查
    "strictBindCallApply": true,             // bind/call/apply 严格类型
    "strictPropertyInitialization": true,    // 类属性必须初始化
    "noImplicitThis": true,                  // this 隐式 any 报错
    "alwaysStrict": true,                    // 严格模式解析 + 输出 "use strict"

    // 进阶严格(TS 4.x+)
    "noUncheckedIndexedAccess": true,        // 索引访问返回 T | undefined
    "exactOptionalPropertyTypes": true,      // 区分 undefined 与可选属性
    "noImplicitReturns": true,               // 所有分支必须有返回值
    "noFallthroughCasesInSwitch": true,      // switch case 禁止贯穿
    "noUncheckedSideEffectImports": true,    // 禁止无副作用的 import

    // 模块与解析
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": true,
    "skipLibCheck": true,                    // 跳过 .d.ts 类型检查(加快编译)
    "resolveJsonModule": true,

    // 声明与输出
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "outDir": "./dist",

    // 路径映射(Monorepo 关键)
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@my-org/shared": ["../packages/shared/src"],
      "@my-org/ui": ["../packages/ui/src"]
    }
  },
  "include": ["src/**/*", "tests/**/*"],
  "exclude": ["node_modules", "dist"]
}

1.2 关键严格选项详解

noUncheckedIndexedAccess

// 开启前(危险)
const users: User[] = fetchUsers();
const first = users[0];        // Type: User(实际可能 undefined)
first.name;                    // 💥 运行时错误!

// 开启后(安全)
const first = users[0];        // Type: User | undefined
first.name;                    // ❌ TS 报错:first 可能为 undefined
first?.name;                   // ✅ 可选链

exactOptionalPropertyTypes

interface Config {
  timeout?: number;
}

// 开启前
const c1: Config = { timeout: undefined }; // ✅ 允许(但实际不需要)

// 开启后
const c1: Config = { timeout: undefined }; // ❌ 报错:timeout 是可选,不是可 undefined

// 正确做法
const c2: Config = {};                     // ✅ 不包含 timeout
const c3: Config = { timeout: 5000 };      // ✅ 有具体值

二、高级类型体操

2.1 条件类型与 infer

// 从 Promise<T> 中提取 T
type Awaited<T> = T extends Promise<infer U> ? U : T;

type Result = Awaited<Promise<string>>; // string
type Result2 = Awaited<number>;          // number

// 提取函数返回值
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

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

2.2 映射类型改造

// 全部变为只读
type Readonly<T> = { readonly [K in keyof T]: T[K] };

// 全部变为可选
type Partial<T> = { [K in keyof T]?: T[K] };

// 全部变为 required(-? 移除可选修饰符)
type Required<T> = { [K in keyof T]-?: T[K] };

// 只选取部分 key
type Pick<T, K extends keyof T> = { [P in K]: T[P] };

// 排除部分 key
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 自定义:所有属性变为 string 或 null
type NullableString<T> = {
  [K in keyof T]: string | null;
};

// 自定义:带前缀的属性名
type PrefixKeys<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K];
};

type User = { name: string; age: number };
type ApiUser = PrefixKeys<User, 'api_'>;
// { api_name: string; api_age: number }

2.3 模板字面量类型

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';

type Endpoint<M extends HttpMethod, P extends string> = `/${Lowercase<M>}/${P}`;

type UserEndpoint = Endpoint<'GET', 'users'>; // "/get/users"

// EventName 映射:onClick → handleClick
type EventHandlers<T extends string> = {
  [K in T as `on${Capitalize<K>}`]: () => void;
};

type Events = EventHandlers<'click' | 'hover' | 'focus'>;
// { onClick: () => void; onHover: () => void; onFocus: () => void }

2.4 递归类型

// 深度 Partial
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};

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

三、类型守卫与 Narrowing

3.1 自定义类型守卫

interface Cat {
  type: 'cat';
  meow(): void;
}

interface Dog {
  type: 'dog';
  bark(): void;
}

type Animal = Cat | Dog;

// 自定义类型守卫函数
function isCat(animal: Animal): animal is Cat {
  return animal.type === 'cat';
}

function makeSound(animal: Animal) {
  if (isCat(animal)) {
    animal.meow(); // ✅ TS 知道是 Cat
  } else {
    animal.bark(); // ✅ TS 知道是 Dog
  }
}

3.2 类型谓词与类守卫

// 非空断言守卫
function isDefined<T>(value: T | undefined | null): value is T {
  return value !== undefined && value !== null;
}

const items = [1, null, 3, undefined, 5];
const valid = items.filter(isDefined); // number[]

// 类实例守卫
class ApiError extends Error {
  constructor(public statusCode: number) {
    super();
  }
}

function handleError(err: unknown) {
  if (err instanceof ApiError) {
    console.log(err.statusCode); // ✅ number
  } else if (err instanceof Error) {
    console.log(err.message);
  }
}

四、泛型约束与协变逆变

4.1 泛型约束

// 约束 T 必须有 length 属性
function logLength<T extends { length: number }>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength('hello');     // ✅ string 有 length
logLength([1, 2, 3]);   // ✅ array 有 length
logLength(123);         // ❌ number 没有 length

// 多泛型约束
type HasId = { id: string };

function updateEntity<T extends HasId>(
  entity: T,
  updates: Partial<Omit<T, 'id'>>
): T {
  return { ...entity, ...updates };
}

const user = { id: 'u1', name: 'Alice', age: 30 };
const updated = updateEntity(user, { name: 'Bob' });
// 不能修改 id!由类型系统保证

4.2 协变与逆变

// 协变(Covariance):子类型可赋值给父类型
interface Animal { name: string }
interface Dog extends Animal { breed: string }

let animals: Animal[] = [];
let dogs: Dog[] = [{ name: 'Buddy', breed: 'Golden' }];

animals = dogs; // ✅ 协变:Dog[] 是 Animal[] 的子类型

// 逆变(Contravariance):函数参数位置
let animalHandler: (a: Animal) => void;
let dogHandler: (d: Dog) => void;

dogHandler = animalHandler; // ✅ 逆变:处理 Animal 的函数可以处理 Dog
// animalHandler = dogHandler; // ❌ 不能反向

五、品牌类型(Branded Types)

用「名义类型」在结构类型系统中区分相同形状的值。

// 用户 ID 和订单 ID 都是 string,但不能混用
type UserId = string & { __brand: 'UserId' };
type OrderId = string & { __brand: 'OrderId' };

function createUserId(id: string): UserId {
  return id as UserId;
}

function createOrderId(id: string): OrderId {
  return id as OrderId;
}

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }

const uid = createUserId('u-123');
const oid = createOrderId('o-456');

getUser(uid);   // ✅
getUser(oid);   // ❌ 类型错误!

六、Monorepo 类型共享

6.1 包内类型导出

// packages/shared/package.json
{
  "name": "@my-org/shared",
  "version": "1.0.0",
  "exports": {
    "./types": {
      "types": "./dist/types/index.d.ts",
      "default": "./dist/types/index.js"
    },
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    }
  }
}
// packages/shared/src/types/api.ts
export interface ApiResponse<T> {
  data: T;
  meta: {
    page: number;
    pageSize: number;
    total: number;
  };
}

export interface ApiError {
  code: string;
  message: string;
  details?: Record<string, string[]>;
}

export type Result<T> =
  | { success: true; data: T }
  | { success: false; error: ApiError };

6.2 应用层消费

// apps/web/src/services/user.ts
import type { ApiResponse, Result } from '@my-org/shared/types';
import type { User } from '@my-org/shared';

export async function fetchUsers(): Promise<Result<User[]>> {
  const res = await fetch('/api/users');
  if (!res.ok) {
    return { success: false, error: await res.json() };
  }
  return { success: true, data: (await res.json()).data };
}

七、API 类型自动同步

7.1 OpenAPI → TypeScript

# 安装
npm install -D openapi-typescript

# 从 OpenAPI JSON 生成类型
npx openapi-typescript https://api.example.com/openapi.json \
  -o src/types/api.ts
// 生成的 api.ts(自动生成,不要手动修改)
export interface paths {
  '/users': {
    get: {
      responses: {
        200: {
          content: {
            'application/json': {
              data: components['schemas']['User'][];
              meta: components['schemas']['PaginationMeta'];
            };
          };
        };
      };
    };
  };
}

export interface components {
  schemas: {
    User: {
      id: string;
      name: string;
      email: string;
      createdAt: string;
    };
  };
}

7.2 类型安全 fetch 封装

import type { paths, components } from './types/api';

// 路径参数提取
type Path = keyof paths;
type Method<P extends Path> = keyof paths[P] & string;
type Response<P extends Path, M extends Method<P>> =
  paths[P][M] extends { responses: infer R }
    ? R extends { 200: { content: { 'application/json': infer D } }
    ? D : never
    : never;

async function apiFetch<P extends Path, M extends Method<P>>(
  path: P,
  method: M
): Promise<Response<P, M>> {
  const res = await fetch(path, { method });
  if (!res.ok) throw new Error(`API error: ${res.status}`);
  return res.json();
}

// 使用:类型自动推导
const users = await apiFetch('/users', 'get');
// users 类型 = { data: User[]; meta: PaginationMeta }

八、Vue 与 React 类型实践

8.1 Vue 3 + TypeScript

<script setup lang="ts">
// 泛型组件 Props
interface Props {
  items: T[];
  labelKey: keyof T;
}

const props = defineProps<Props>();

const emit = defineEmits<{
  select: [item: T];
  remove: [index: number];
}>();

// 类型安全的 ref
const selectedIndex = ref<number | null>(null);

// 类型安全的 computed
const selectedItem = computed(() => {
  if (selectedIndex.value === null) return null;
  return props.items[selectedIndex.value]; // noUncheckedIndexedAccess 确保安全
});
</script>

8.2 React + TypeScript

// 泛型组件
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// 使用:类型自动推导
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

// forwardRef + 类型
import { forwardRef } from 'react';

interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
  variant?: 'primary' | 'secondary';
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ variant = 'primary', ...props }, ref) => {
    return (
      <button
        ref={ref}
        className={`btn btn--${variant}`}
        {...props}
      />
    );
  }
);
Button.displayName = 'Button';

九、类型安全 Checklist

检查项配置/做法收益
开启 strict 全家桶"strict": true消除隐式 any
索引访问安全noUncheckedIndexedAccess防数组越界
可选属性精确exactOptionalPropertyTypes区分可选与 undefined
API 类型同步openapi-typescript前后端类型一致
品牌类型type UserId = string & { __brand: 'UserId' }防 ID 混用
Monorepo 类型共享包内 exports.types + paths 映射跨包类型安全
类型守卫函数function isX(v): v is X运行时 + 编译时安全

参考与延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. API 设计与 BFF 层:REST、GraphQL、tRPC 选型与前后端协作
  2. WebAssembly 前端工程化实践:编译链、性能对比与混合架构
  3. 现代浏览器 API 与 Web 平台能力地图