TypeScript高级类型系统与工程化实践

TypeScript 不仅仅是为 JavaScript 添加类型注解,它提供了一套完整的类型系统,让我们可以在编译时捕获错误,提高代码质量和开发效率。但很多开发者只停留在基础的类型注解阶段,没有充分利用 TypeScript 的高级特性。

本文将深入探讨 TypeScript 的高级类型系统,包括泛型、条件类型、映射类型等,以及如何在实际项目中应用这些特性来构建类型安全的应用程序。


一、泛型:类型参数化的艺术

🎯 核心理念:泛型让我们可以创建可复用的组件,同时保持类型安全。

基础泛型

// 基础泛型函数
function identity<T>(arg: T): T {
  return arg
}

// 使用泛型
const stringResult = identity<string>('hello') // 类型: string
const numberResult = identity<number>(42) // 类型: number

// 类型推断
const inferred = identity('hello') // TypeScript 自动推断为 string

泛型接口

// 泛型接口
interface ApiResponse<T> {
  data: T
  status: number
  message: string
}

// 使用泛型接口
interface User {
  id: number
  name: string
  email: string
}

interface Product {
  id: number
  title: string
  price: number
}

// 类型化的 API 响应
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: '张三', email: '[email protected]' },
  status: 200,
  message: 'success',
}

const productResponse: ApiResponse<Product> = {
  data: { id: 1, title: 'iPhone', price: 5999 },
  status: 200,
  message: 'success',
}

泛型约束

// 泛型约束:限制泛型参数必须具有某些属性
interface Lengthwise {
  length: number
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length)
  return arg
}

// 使用约束
logLength('hello') // 字符串有 length 属性
logLength([1, 2, 3]) // 数组有 length 属性
// logLength(123); // 错误:数字没有 length 属性

// 更复杂的约束
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: '张三', age: 25, email: '[email protected]' }
const name = getProperty(user, 'name') // 类型: string
const age = getProperty(user, 'age') // 类型: number
// const invalid = getProperty(user, "invalid"); // 错误:属性不存在

二、条件类型:类型级别的条件判断

🧠 核心理念:条件类型让我们可以根据其他类型来动态选择类型。

基础条件类型

// 基础条件类型语法
type IsString<T> = T extends string ? true : false

// 使用条件类型
type Test1 = IsString<string> // true
type Test2 = IsString<number> // false

// 更实用的例子:提取数组元素类型
type ArrayElement<T> = T extends (infer U)[] ? U : never

type StringArray = string[]
type ElementType = ArrayElement<StringArray> // string

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

function getUser(): { id: number; name: string } {
  return { id: 1, name: '张三' }
}

type UserType = ReturnType<typeof getUser> // { id: number; name: string }

分布式条件类型

// 分布式条件类型:当条件类型作用于联合类型时
type ToArray<T> = T extends any ? T[] : never

type StringOrNumber = ToArray<string | number> // string[] | number[]

// 过滤联合类型
type Filter<T, U> = T extends U ? T : never

type AllowedTypes = string | number | boolean
type StringOnly = Filter<AllowedTypes, string> // string

// 提取非空类型
type NonNullable<T> = T extends null | undefined ? never : T

type MaybeString = string | null | undefined
type DefiniteString = NonNullable<MaybeString> // string

递归条件类型

// 递归条件类型:处理嵌套结构
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
}

interface User {
  id: number
  name: string
  profile: {
    avatar: string
    settings: {
      theme: string
      notifications: boolean
    }
  }
}

type ReadonlyUser = DeepReadonly<User>
// 结果:所有属性都变为 readonly,包括嵌套对象

// 深度可选
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}

type PartialUser = DeepPartial<User>
// 结果:所有属性都变为可选,包括嵌套对象

三、映射类型:批量转换类型

🔄 核心理念:映射类型让我们可以基于现有类型创建新类型。

基础映射类型

// 基础映射类型语法
type Partial<T> = {
  [P in keyof T]?: T[P]
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
}

// 使用映射类型
interface User {
  id: number
  name: string
  email: string
}

type PartialUser = Partial<User> // 所有属性可选
type RequiredUser = Required<User> // 所有属性必需
type ReadonlyUser = Readonly<User> // 所有属性只读

高级映射类型

// 键重映射
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface User {
  name: string
  age: number
}

type UserGetters = Getters<User>
// 结果:{ getName: () => string; getAge: () => number; }

// 过滤属性
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
}

interface MixedObject {
  name: string
  age: number
  isActive: boolean
  createdAt: Date
}

type StringProps = PickByType<MixedObject, string> // { name: string }
type NumberProps = PickByType<MixedObject, number> // { age: number }

// 条件键重映射
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void
}

interface FormData {
  username: string
  email: string
  password: string
}

type FormEventHandlers = EventHandlers<FormData>
// 结果:{ onUsername: (value: string) => void; onEmail: (value: string) => void; onPassword: (value: string) => void; }

四、模板字面量类型:字符串类型操作

📝 核心理念:模板字面量类型让我们可以在类型级别操作字符串。

基础模板字面量类型

// 基础模板字面量类型
type Greeting = `Hello, ${string}!`

const validGreeting: Greeting = 'Hello, World!' // 正确
const invalidGreeting: Greeting = 'Hi, World!' // 错误

// 更复杂的例子
type EventName<T extends string> = `on${Capitalize<T>}`

type ClickEvent = EventName<'click'> // "onClick"
type SubmitEvent = EventName<'submit'> // "onSubmit"

// 联合类型模板
type SupportedEvents = 'click' | 'submit' | 'change'
type EventHandlers = EventName<SupportedEvents> // "onClick" | "onSubmit" | "onChange"

高级模板字面量类型

// 字符串分割
type Split<S extends string, D extends string> = string extends S
  ? string[]
  : S extends ''
    ? []
    : S extends `${infer T}${D}${infer U}`
      ? [T, ...Split<U, D>]
      : [S]

type PathParts = Split<'user/profile/settings', '/'> // ["user", "profile", "settings"]

// 字符串替换
type Replace<
  S extends string,
  From extends string,
  To extends string,
> = S extends `${infer L}${From}${infer R}` ? `${L}${To}${R}` : S

type UpdatedPath = Replace<'user/profile', 'profile', 'settings'> // "user/settings"

// 字符串长度
type Length<S extends string> = Split<S, ''>['length']

type NameLength = Length<'TypeScript'> // 10

五、类型体操:复杂类型操作

🏋️ 核心理念:类型体操让我们可以创建复杂的类型操作,提高代码的类型安全性。

数组操作类型

// 数组长度
type Length<T extends readonly any[]> = T['length']

type ArrLength = Length<[1, 2, 3, 4, 5]> // 5

// 数组连接
type Concat<A extends readonly any[], B extends readonly any[]> = [...A, ...B]

type ConcatResult = Concat<[1, 2], [3, 4]> // [1, 2, 3, 4]

// 数组反转
type Reverse<T extends readonly any[]> = T extends readonly [infer First, ...infer Rest]
  ? [...Reverse<Rest>, First]
  : []

type Reversed = Reverse<[1, 2, 3, 4]> // [4, 3, 2, 1]

// 数组去重
type Unique<T extends readonly any[], Result extends readonly any[] = []> = T extends readonly [
  infer First,
  ...infer Rest,
]
  ? First extends Result[number]
    ? Unique<Rest, Result>
    : Unique<Rest, [...Result, First]>
  : Result

type UniqueArray = Unique<[1, 2, 2, 3, 3, 3, 4]> // [1, 2, 3, 4]

对象操作类型

// 对象键值对
type Entries<T> = {
  [K in keyof T]: [K, T[K]]
}[keyof T]

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

type UserEntries = Entries<User> // ["name", string] | ["age", number] | ["email", string]

// 对象值类型
type ValueOf<T> = T[keyof T]

type UserValues = ValueOf<User> // string | number

// 深度合并
type DeepMerge<T, U> = {
  [K in keyof T | keyof U]: K extends keyof U
    ? K extends keyof T
      ? T[K] extends object
        ? U[K] extends object
          ? DeepMerge<T[K], U[K]>
          : U[K]
        : U[K]
      : U[K]
    : K extends keyof T
      ? T[K]
      : never
}

interface BaseConfig {
  api: {
    baseUrl: string
    timeout: number
  }
  ui: {
    theme: string
  }
}

interface OverrideConfig {
  api: {
    timeout: number
  }
  ui: {
    theme: string
    language: string
  }
}

type MergedConfig = DeepMerge<BaseConfig, OverrideConfig>
// 结果:深度合并两个配置对象

六、工程化实践:类型安全的项目架构

1. 类型定义组织

// types/index.ts - 类型定义入口
export * from './api'
export * from './user'
export * from './product'
export * from './common'

// types/api.ts - API 相关类型
export interface ApiResponse<T = any> {
  data: T
  status: number
  message: string
  timestamp: number
}

export interface ApiError {
  code: string
  message: string
  details?: Record<string, any>
}

export type ApiResult<T> = ApiResponse<T> | ApiError

// types/user.ts - 用户相关类型
export interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: Date
  updatedAt: Date
}

export interface CreateUserRequest {
  name: string
  email: string
  password: string
}

export interface UpdateUserRequest {
  name?: string
  email?: string
  avatar?: string
}

// types/product.ts - 产品相关类型
export interface Product {
  id: number
  title: string
  description: string
  price: number
  category: ProductCategory
  images: string[]
  inStock: boolean
}

export interface ProductCategory {
  id: number
  name: string
  slug: string
}

// types/common.ts - 通用类型
export type ID = string | number

export interface PaginationParams {
  page: number
  limit: number
  sort?: string
  order?: 'asc' | 'desc'
}

export interface PaginatedResponse<T> {
  data: T[]
  pagination: {
    page: number
    limit: number
    total: number
    totalPages: number
  }
}

2. 类型安全的 API 客户端

// api/client.ts
import { ApiResponse, ApiError, ApiResult } from '../types'

class ApiClient {
  private baseUrl: string
  private defaultHeaders: Record<string, string>

  constructor(baseUrl: string, defaultHeaders: Record<string, string> = {}) {
    this.baseUrl = baseUrl
    this.defaultHeaders = {
      'Content-Type': 'application/json',
      ...defaultHeaders,
    }
  }

  async request<T>(endpoint: string, options: RequestInit = {}): Promise<ApiResult<T>> {
    try {
      const response = await fetch(`${this.baseUrl}${endpoint}`, {
        ...options,
        headers: {
          ...this.defaultHeaders,
          ...options.headers,
        },
      })

      const data = await response.json()

      if (!response.ok) {
        return {
          code: data.code || 'UNKNOWN_ERROR',
          message: data.message || 'An error occurred',
          details: data.details,
        }
      }

      return {
        data: data.data || data,
        status: response.status,
        message: data.message || 'Success',
        timestamp: Date.now(),
      }
    } catch (error) {
      return {
        code: 'NETWORK_ERROR',
        message: error instanceof Error ? error.message : 'Network error',
      }
    }
  }

  // 类型安全的 GET 请求
  async get<T>(endpoint: string): Promise<ApiResult<T>> {
    return this.request<T>(endpoint, { method: 'GET' })
  }

  // 类型安全的 POST 请求
  async post<T, U = any>(endpoint: string, data: U): Promise<ApiResult<T>> {
    return this.request<T>(endpoint, {
      method: 'POST',
      body: JSON.stringify(data),
    })
  }

  // 类型安全的 PUT 请求
  async put<T, U = any>(endpoint: string, data: U): Promise<ApiResult<T>> {
    return this.request<T>(endpoint, {
      method: 'PUT',
      body: JSON.stringify(data),
    })
  }

  // 类型安全的 DELETE 请求
  async delete<T>(endpoint: string): Promise<ApiResult<T>> {
    return this.request<T>(endpoint, { method: 'DELETE' })
  }
}

// 使用示例
const apiClient = new ApiClient('https://api.example.com')

// 类型安全的 API 调用
const userResult = await apiClient.get<User>('/users/1')
if ('data' in userResult) {
  console.log(userResult.data.name) // 类型安全
} else {
  console.error(userResult.message) // 错误处理
}

3. 类型安全的组件 Props

// components/Button.tsx
import React from 'react';

// 按钮变体类型
type ButtonVariant = 'primary' | 'secondary' | 'danger' | 'success';
type ButtonSize = 'small' | 'medium' | 'large';

// 基础按钮属性
interface BaseButtonProps {
  children: React.ReactNode;
  variant?: ButtonVariant;
  size?: ButtonSize;
  disabled?: boolean;
  loading?: boolean;
  onClick?: () => void;
}

// 链接按钮属性
interface LinkButtonProps extends BaseButtonProps {
  href: string;
  target?: '_blank' | '_self' | '_parent' | '_top';
}

// 提交按钮属性
interface SubmitButtonProps extends BaseButtonProps {
  type: 'submit';
  form?: string;
}

// 按钮属性联合类型
type ButtonProps = LinkButtonProps | SubmitButtonProps | BaseButtonProps;

// 类型守卫
function isLinkButton(props: ButtonProps): props is LinkButtonProps {
  return 'href' in props;
}

function isSubmitButton(props: ButtonProps): props is SubmitButtonProps {
  return 'type' in props && props.type === 'submit';
}

// 按钮组件
export const Button: React.FC<ButtonProps> = (props) => {
  const { children, variant = 'primary', size = 'medium', disabled, loading } = props;

  const baseClasses = 'btn';
  const variantClasses = `btn--${variant}`;
  const sizeClasses = `btn--${size}`;
  const stateClasses = disabled ? 'btn--disabled' : '';
  const loadingClasses = loading ? 'btn--loading' : '';

  const className = [baseClasses, variantClasses, sizeClasses, stateClasses, loadingClasses]
    .filter(Boolean)
    .join(' ');

  if (isLinkButton(props)) {
    return (
      <a
        href={props.href}
        target={props.target}
        className={className}
        onClick={props.onClick}
      >
        {loading ? '加载中...' : children}
      </a>
    );
  }

  if (isSubmitButton(props)) {
    return (
      <button
        type="submit"
        form={props.form}
        className={className}
        disabled={disabled || loading}
        onClick={props.onClick}
      >
        {loading ? '加载中...' : children}
      </button>
    );
  }

  return (
    <button
      type="button"
      className={className}
      disabled={disabled || loading}
      onClick={props.onClick}
    >
      {loading ? '加载中...' : children}
    </button>
  );
};

// 使用示例
const App: React.FC = () => {
  return (
    <div>
      <Button variant="primary" size="large" onClick={() => console.log('clicked')}>
        点击我
      </Button>

      <Button href="/about" variant="secondary">
        关于我们
      </Button>

      <Button type="submit" form="myForm" variant="success">
        提交表单
      </Button>
    </div>
  );
};

4. 类型安全的 Redux Store

// store/types.ts
export interface RootState {
  user: UserState
  products: ProductsState
  cart: CartState
}

export interface UserState {
  currentUser: User | null
  loading: boolean
  error: string | null
}

export interface ProductsState {
  items: Product[]
  loading: boolean
  error: string | null
  filters: ProductFilters
}

export interface CartState {
  items: CartItem[]
  total: number
  loading: boolean
}

export interface CartItem {
  productId: number
  quantity: number
  price: number
}

export interface ProductFilters {
  category?: string
  minPrice?: number
  maxPrice?: number
  inStock?: boolean
}

// store/actions.ts
export const userActions = {
  setUser: (user: User) => ({ type: 'user/setUser', payload: user }),
  setLoading: (loading: boolean) => ({ type: 'user/setLoading', payload: loading }),
  setError: (error: string | null) => ({ type: 'user/setError', payload: error }),
} as const

export const productActions = {
  setProducts: (products: Product[]) => ({ type: 'products/setProducts', payload: products }),
  setFilters: (filters: ProductFilters) => ({ type: 'products/setFilters', payload: filters }),
  setLoading: (loading: boolean) => ({ type: 'products/setLoading', payload: loading }),
  setError: (error: string | null) => ({ type: 'products/setError', payload: error }),
} as const

export const cartActions = {
  addItem: (item: CartItem) => ({ type: 'cart/addItem', payload: item }),
  removeItem: (productId: number) => ({ type: 'cart/removeItem', payload: productId }),
  updateQuantity: (productId: number, quantity: number) => ({
    type: 'cart/updateQuantity',
    payload: { productId, quantity },
  }),
  clearCart: () => ({ type: 'cart/clearCart' }),
} as const

// store/reducers.ts
import { Reducer } from 'redux'

const userReducer: Reducer<UserState> = (state = initialState.user, action) => {
  switch (action.type) {
    case 'user/setUser':
      return { ...state, currentUser: action.payload }
    case 'user/setLoading':
      return { ...state, loading: action.payload }
    case 'user/setError':
      return { ...state, error: action.payload }
    default:
      return state
  }
}

const productsReducer: Reducer<ProductsState> = (state = initialState.products, action) => {
  switch (action.type) {
    case 'products/setProducts':
      return { ...state, items: action.payload }
    case 'products/setFilters':
      return { ...state, filters: { ...state.filters, ...action.payload } }
    case 'products/setLoading':
      return { ...state, loading: action.payload }
    case 'products/setError':
      return { ...state, error: action.payload }
    default:
      return state
  }
}

const cartReducer: Reducer<CartState> = (state = initialState.cart, action) => {
  switch (action.type) {
    case 'cart/addItem':
      const existingItem = state.items.find((item) => item.productId === action.payload.productId)
      if (existingItem) {
        return {
          ...state,
          items: state.items.map((item) =>
            item.productId === action.payload.productId
              ? { ...item, quantity: item.quantity + action.payload.quantity }
              : item
          ),
        }
      }
      return { ...state, items: [...state.items, action.payload] }
    case 'cart/removeItem':
      return {
        ...state,
        items: state.items.filter((item) => item.productId !== action.payload),
      }
    case 'cart/updateQuantity':
      return {
        ...state,
        items: state.items.map((item) =>
          item.productId === action.payload.productId
            ? { ...item, quantity: action.payload.quantity }
            : item
        ),
      }
    case 'cart/clearCart':
      return { ...state, items: [] }
    default:
      return state
  }
}

// store/selectors.ts
import { createSelector } from '@reduxjs/toolkit'

// 基础选择器
const selectUser = (state: RootState) => state.user
const selectProducts = (state: RootState) => state.products
const selectCart = (state: RootState) => state.cart

// 派生选择器
export const selectCurrentUser = createSelector(selectUser, (user) => user.currentUser)

export const selectFilteredProducts = createSelector(selectProducts, (products) => {
  const { items, filters } = products
  return items.filter((product) => {
    if (filters.category && product.category.slug !== filters.category) {
      return false
    }
    if (filters.minPrice && product.price < filters.minPrice) {
      return false
    }
    if (filters.maxPrice && product.price > filters.maxPrice) {
      return false
    }
    if (filters.inStock !== undefined && product.inStock !== filters.inStock) {
      return false
    }
    return true
  })
})

export const selectCartTotal = createSelector(selectCart, (cart) =>
  cart.items.reduce((total, item) => total + item.price * item.quantity, 0)
)

export const selectCartItemCount = createSelector(selectCart, (cart) =>
  cart.items.reduce((count, item) => count + item.quantity, 0)
)

七、类型安全的测试

// tests/utils/test-utils.ts
import { render, RenderOptions } from '@testing-library/react';
import { ReactElement } from 'react';
import { Provider } from 'react-redux';
import { configureStore } from '@reduxjs/toolkit';
import { RootState } from '../store/types';

// 创建测试用的 store
export const createTestStore = (preloadedState?: Partial<RootState>) => {
  return configureStore({
    reducer: {
      user: (state = { currentUser: null, loading: false, error: null }) => state,
      products: (state = { items: [], loading: false, error: null, filters: {} }) => state,
      cart: (state = { items: [], total: 0, loading: false }) => state
    },
    preloadedState
  });
};

// 自定义渲染函数
export const renderWithProviders = (
  ui: ReactElement,
  {
    preloadedState,
    store = createTestStore(preloadedState),
    ...renderOptions
  }: {
    preloadedState?: Partial<RootState>;
    store?: ReturnType<typeof createTestStore>;
  } & Omit<RenderOptions, 'wrapper'> = {}
) => {
  const Wrapper = ({ children }: { children: React.ReactNode }) => (
    <Provider store={store}>{children}</Provider>
  );

  return { store, ...render(ui, { wrapper: Wrapper, ...renderOptions }) };
};

// 测试辅助函数
export const createMockUser = (overrides: Partial<User> = {}): User => ({
  id: 1,
  name: 'Test User',
  email: '[email protected]',
  avatar: 'https://example.com/avatar.jpg',
  createdAt: new Date('2023-01-01'),
  updatedAt: new Date('2023-01-01'),
  ...overrides
});

export const createMockProduct = (overrides: Partial<Product> = {}): Product => ({
  id: 1,
  title: 'Test Product',
  description: 'A test product',
  price: 99.99,
  category: { id: 1, name: 'Test Category', slug: 'test-category' },
  images: ['https://example.com/image.jpg'],
  inStock: true,
  ...overrides
});

八、常见问题与解决方案

问题1:类型断言过度使用

// 错误做法:过度使用类型断言
const user = getUser() as User // 危险:可能运行时出错

// 正确做法:使用类型守卫
function isUser(obj: any): obj is User {
  return obj && typeof obj.id === 'number' && typeof obj.name === 'string'
}

const userData = getUser()
if (isUser(userData)) {
  // 现在 userData 被正确推断为 User 类型
  console.log(userData.name)
}

问题2:any 类型滥用

// 错误做法:使用 any 类型
function processData(data: any): any {
  return data.someProperty
}

// 正确做法:使用泛型或具体类型
function processData<T>(data: T): T {
  return data
}

// 或者使用 unknown 类型
function processData(data: unknown): string {
  if (typeof data === 'string') {
    return data
  }
  throw new Error('Expected string')
}

问题3:类型定义不完整

// 错误做法:类型定义不完整
interface User {
  name: string
  // 缺少其他必要属性
}

// 正确做法:完整的类型定义
interface User {
  id: number
  name: string
  email: string
  avatar?: string
  createdAt: Date
  updatedAt: Date
}

// 或者使用工具类型
type PartialUser = Partial<User> // 所有属性可选
type RequiredUser = Required<User> // 所有属性必需

结语

TypeScript 的高级类型系统为我们提供了强大的工具来构建类型安全的应用程序。通过合理使用泛型、条件类型、映射类型等特性,我们可以:

  1. 提高代码质量:在编译时捕获更多错误
  2. 增强开发体验:更好的 IDE 支持和自动补全
  3. 降低维护成本:类型作为文档,减少理解成本
  4. 提高团队协作效率:统一的类型定义和约定

但也要注意不要过度使用复杂的类型操作,保持代码的可读性和可维护性。记住:类型系统是工具,不是目的,最终目标是为业务服务