阿里云主机折上折
  • 微信号
Current Site:Index > Best practices for type programming

Best practices for type programming

Author:Chuan Chen 阅读数:41178人阅读 分类: TypeScript

Type programming is one of the core capabilities of TypeScript. By leveraging the type system effectively, you can significantly enhance the robustness and maintainability of your code. From basic type operations to advanced pattern matching, the practices of type programming are diverse, with the key lying in understanding the underlying logic of the type system and real-world application scenarios.

Basic Type Operations and Utility Types

TypeScript comes with a wealth of built-in utility types, and using them appropriately can reduce repetitive code. For example, Partial<T> can make all properties optional:

interface User {
  id: number
  name: string
}

type PartialUser = Partial<User>
// Equivalent to { id?: number; name?: string }

Pick and Omit are powerful tools for working with object types:

type UserName = Pick<User, 'name'> // { name: string }
type UserWithoutId = Omit<User, 'id'> // { name: string }

When creating custom utility types, conditional types are fundamental building blocks:

type Nullable<T> = T | null
type ValueOf<T> = T[keyof T]

Type Inference and Pattern Matching

The infer keyword is invaluable for deconstructing complex types. For example, extracting a function's return type:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never

function getUser() { return { id: 1 } }
type UserReturn = ReturnType<typeof getUser> // { id: number }

Recursive types excel when working with array types:

type Flatten<T> = T extends Array<infer U> ? Flatten<U> : T
type Nested = number[][][]
type Flat = Flatten<Nested> // number

Practical Use of Template Literal Types

Introduced in version 4.1, template literal types enable precise string constraints:

type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type ApiPath = `/${string}`

type Endpoint = `${HttpMethod} ${ApiPath}`
const endpoint: Endpoint = 'GET /users' // Valid

Combined with mapped types, they can generate dynamic properties:

type EventMap = {
  click: MouseEvent
  focus: FocusEvent
}

type HandlerMap = {
  [K in keyof EventMap as `on${Capitalize<K>}`]: (event: EventMap[K]) => void
}
// Generates { onClick: (event: MouseEvent) => void; onFocus: (event: FocusEvent) => void }

Type Guards and Discriminated Unions

Custom type guards significantly improve type narrowing:

function isStringArray(value: unknown): value is string[] {
  return Array.isArray(value) && value.every(item => typeof item === 'string')
}

const data: unknown = ['a', 'b']
if (isStringArray(data)) {
  data.map(s => s.toUpperCase()) // Safe to call
}

Discriminated Unions are a best practice for handling complex states:

type Result<T> = 
  | { status: 'success'; data: T }
  | { status: 'error'; message: string }

function handleResult(result: Result<number>) {
  switch (result.status) {
    case 'success':
      console.log(result.data * 2) // Automatically narrowed
      break
    case 'error':
      console.error(result.message)
      break
  }
}

Advanced Patterns: Type-Level Programming

Implementing type-safe Curry functions requires deep type manipulation:

type Curry<F> = F extends (...args: infer A) => infer R
  ? A extends [infer First, ...infer Rest]
    ? (arg: First) => Curry<(...args: Rest) => R>
    : R
  : never

function curry<F>(fn: F): Curry<F> {
  // Implementation omitted
}

const add = (a: number, b: number) => a + b
const curriedAdd = curry(add)
const add5 = curriedAdd(5) // (b: number) => number

Type predicates can create complex validation logic:

type IPv4 = `${number}.${number}.${number}.${number}`

function isIPv4(str: string): str is IPv4 {
  const parts = str.split('.')
  return parts.length === 4 && parts.every(part => {
    const num = parseInt(part, 10)
    return num >= 0 && num <= 255 && part === num.toString()
  })
}

Performance Optimization and Type Caching

Complex type operations can degrade compiler performance, necessitating type caching:

type DeepReadonly<T> = 
  T extends Function ? T :
  T extends object ? { readonly [K in keyof T]: DeepReadonly<T[K]> } :
  T

// Use interfaces to cache intermediate types
interface ReadonlyUser extends DeepReadonly<User> {}

Avoid excessive use of recursive types and set depth limits when necessary:

type DeepPartial<T, Depth extends number = 3> = 
  [Depth] extends [0] ? T :
  T extends object ? { [K in keyof T]?: DeepPartial<T[K], [-1, 0, 1, 2][Depth]> } : 
  T

Synchronizing Types and Runtime Values

Use const assertions to keep types and values in sync:

const routes = ['home', 'users', 'settings'] as const
type Route = typeof routes[number] // "home" | "users" | "settings"

Create type-safe alternatives to enums:

function createEnum<T extends string>(...values: T[]) {
  return Object.freeze(values.reduce((acc, val) => {
    acc[val] = val
    return acc
  }, {} as { [K in T]: K }))
}

const Direction = createEnum('North', 'South', 'East', 'West')
type Direction = keyof typeof Direction // "North" | "South" | "East" | "West"

Handling Type System Boundaries

Flexible type design is required for dynamic data structures:

type JSONValue = 
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue }

function safeParse(json: string): unknown {
  try {
    return JSON.parse(json)
  } catch {
    return undefined
  }
}

const data = safeParse('{"a":1}')
if (data && typeof data === 'object' && 'a' in data) {
  // Type narrowing
}

For extending third-party library types, module augmentation is the standard approach:

declare module 'some-library' {
  interface Config {
    customOption?: boolean
  }
}

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.