阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!

過去幾個月,我一直在建立UserJot ,一個面向 SaaS 團隊的回饋和路線圖平台。這個專案讓我對 TypeScript 有了深入的了解——在此之前,我偶爾會使用它,但從未達到如此大規模。經過長時間的 TypeScript 生產程式碼編寫、類型錯誤除錯以及為了提高類型安全性而進行的重構,我掌握了一些技巧,這些技巧確實能加快開發速度,並在 bug 進入生產環境之前就將其捕獲。

UserJot 儀表板

這些並非教科書上的理論模式,而是我每天都在使用的實用技巧,都是在實際解決問題的過程中發現的。有些技巧可以幫助我避免執行時錯誤,有些則能讓程式碼更簡潔、更易用。以下是 20 個真正有用的技巧。

  1. 使用satisfies實現更好的類型推斷

satisfies運算子允許你驗證表達式是否符合類型,同時保留字面量的類型。這對於需要兼顧類型安全性和精確推斷的配置物件尤其有用。

// Without satisfies - loses specific types
const config1: Record<string, string | number> = {
  port: 3000,
  host: 'localhost'
}
// config1.port is string | number

// With satisfies - keeps specific types
const config2 = {
  port: 3000,
  host: 'localhost'
} satisfies Record<string, string | number>
// config2.port is number, config2.host is string
  1. 不可變型別的 Const 斷言

為物件新增as const ,TypeScript 會將所有屬性視為唯讀,並推斷出盡可能具體的類型。這對於不應更改的配置資料非常有用。

const routes = {
  home: '/',
  dashboard: '/dashboard',
  settings: '/settings'
} as const
// routes.home is '/' not string
  1. 字串模式的模板字面量類型

模板字面量類型可讓您建立與特定字串模式相符的類型。非常適合 API 端點、事件名稱或任何結構化字串。

type EventName = `on${Capitalize<string>}`
// 'onClick', 'onChange', 'onSubmit' ✓
// 'click', 'handleClick' ✗

type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = `/api/${string}`
type Route = `${Method} ${Endpoint}`
// 'GET /api/users' ✓
// 'GET /users' ✗

// Real world example
function makeRequest(route: Route) {
  // TypeScript ensures route follows pattern
}

makeRequest('GET /api/users') // ✓
makeRequest('GET /users') // ✗ Error
  1. 歧視性工會爭取國家管理

使用通用屬性來區分聯合成員。 TypeScript 會根據此區分符縮小類型範圍,從而提高程式碼的安全性。

type State = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error }

function handleState(state: State) {
  switch (state.status) {
    case 'idle':
      // TypeScript knows state is { status: 'idle' }
      break
    case 'loading':
      // TypeScript knows state is { status: 'loading' }
      break
    case 'success':
      // TypeScript knows state has 'data' property
      console.log(state.data.toUpperCase())
      break
    case 'error':
      // TypeScript knows state has 'error' property
      console.error(state.error.message)
      break
  }
}
  1. 自訂類型保護的類型謂詞

建立函數來告訴 TypeScript 某個物件是什麼類型。這比散佈在程式碼中的多個 typeof 檢查要簡潔得多。

function isString(value: unknown): value is string {
  return typeof value === 'string'
}

6.索引存取類型

使用括號表示法從其他類型中提取類型。這可以保持類型的 DRY 原則,並在來源類型變更時自動更新。

type User = { id: string; name: string; email: string }
type UserEmail = User['email'] // string
type UserKeys = keyof User // 'id' | 'name' | 'email'
  1. 動態類型邏輯的條件類型

使用條件類型來建立根據條件變化的類型。可以將它們視為類型的三元運算子。

type IsArray<T> = T extends any[] ? true : false
type Test1 = IsArray<string[]> // true
type Test2 = IsArray<string> // false

// Extract array element type
type Flatten<T> = T extends Array<infer U> ? U : T
type Flattened1 = Flatten<string[]> // string
type Flattened2 = Flatten<number> // number

// More practical example
type ApiResponse<T> = T extends { error: string }
  ? { success: false; error: string }
  : { success: true; data: T }
  1. 實用類型是你的朋友

TypeScript 內建了實用類型,可以解決常見問題。學習它們,而不是重新發明輪子。

type PartialUser = Partial<User> // All properties optional
type ReadonlyUser = Readonly<User> // All properties readonly
type UserWithoutEmail = Omit<User, 'email'>
type JustEmailAndId = Pick<User, 'email' | 'id'>
  1. 函數重載,實現更好的 DX

為不同的用例提供多個函數簽名。這能為函數的使用者提供更好的自動補全和類型檢查功能。

// Overload signatures
function parse(value: string): object
function parse(value: string, reviver: Function): object
// Implementation signature (not visible to consumers)
function parse(value: string, reviver?: Function) {
  return JSON.parse(value, reviver)
}

// Usage gets proper type hints
const obj1 = parse('{}') // return type is object
const obj2 = parse('{}', (k, v) => v) // knows reviver is allowed

// Another example with different return types
function createElement(tag: 'img'): HTMLImageElement
function createElement(tag: 'input'): HTMLInputElement
function createElement(tag: string): HTMLElement
function createElement(tag: string): HTMLElement {
  return document.createElement(tag)
}

const img = createElement('img') // type is HTMLImageElement
const input = createElement('input') // type is HTMLInputElement
  1. 通用約束

限制可以傳遞給泛型的類型。這可以避免錯誤並提供更好的 IntelliSense。

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}
  1. 轉換的映射類型

系統地轉換一個類型的所有屬性。非常適合建立現有類型的變體。

// Make all properties nullable
type Nullable<T> = { [K in keyof T]: T[K] | null }

type User = { id: string; name: string; age: number }
type NullableUser = Nullable<User>
// { id: string | null; name: string | null; age: number | null }

// Create getter methods from properties
type Getters<T> = { 
  [K in keyof T as `get${Capitalize<K & string>}`]: () => T[K] 
}

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

// Remove readonly modifier
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
  1. 切勿進行詳盡的檢查

使用never確保你已處理 switch 語句中的所有情況。如果你漏掉了一個狀況,TypeScript 就會報錯。

function handleStatus(status: State['status']) {
  switch (status) {
    case 'idle': return
    case 'loading': return
    case 'success': return
    case 'error': return
    default:
      const _exhaustive: never = status
      throw new Error(`Unhandled status: ${status}`)
  }
}
  1. 模組擴充

根據需要擴充現有模組類型。適用於新增屬性或擴充第三方程式庫。

declare global {
  interface Window {
    analytics: AnalyticsClient
  }
}
  1. 僅類型導入

使用僅類型導入來確保導入在執行時被移除。這可以減少套件的大小並防止循環依賴。

import type { User } from './types'
import { type Config, validateConfig } from './config'
  1. 斷言函數

建立斷言條件和縮小類型的函數。如果斷言失敗,函數將拋出異常。這對於在執行時驗證資料並同時保證 TypeScript 正常運作非常有用。

function assertDefined<T>(value: T | undefined): asserts value is T {
  if (value === undefined) {
    throw new Error('Value is undefined')
  }
}

// Usage
function processUser(user: User | undefined) {
  assertDefined(user)
  // TypeScript now knows user is User, not User | undefined
  console.log(user.name.toUpperCase())
}

// Multiple assertions
function assertValidEmail(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Email must be string')
  }
  if (!value.includes('@')) {
    throw new Error('Invalid email format')
  }
}
  1. 執行時安全的品牌類型

建立結構相同但名義不同的類型。避免混淆相似的原始型別。這種模式讓我避免了許多傳遞錯誤 ID 類型的錯誤。

type UserId = string & { __brand: 'UserId' }
type PostId = string & { __brand: 'PostId' }

// Helper functions to create branded types
function userId(id: string): UserId {
  return id as UserId
}

function postId(id: string): PostId {
  return id as PostId
}

// Usage
function getUserById(id: UserId) { /* ... */ }
function getPostById(id: PostId) { /* ... */ }

const uId = userId('user_123')
const pId = postId('post_456')

getUserById(uId) // ✓
getUserById(pId) // ✗ Error: Argument of type 'PostId' not assignable to 'UserId'
  1. 具有流暢介面的建構器模式

使用泛型追蹤建構器的狀態,並確保方法以正確的順序呼叫。此模式可確保方法鏈的編譯時安全性。

class QueryBuilder<T = {}> {
  private query: any = {}

  select<K extends string>(field: K): QueryBuilder<T & { select: K }> {
    this.query.select = field
    return this as any
  }

  where<K extends string>(field: K): QueryBuilder<T & { where: K }> {
    this.query.where = field
    return this as any
  }

  // Only available when both select and where are called
  build(this: QueryBuilder<{ select: string; where: string }>) {
    return this.query
  }
}

// Usage
const query = new QueryBuilder()
  .select('name')
  .where('id')
  .build() // ✓ Works

const badQuery = new QueryBuilder()
  .select('name')
  .build() // ✗ Error: build() requires where() to be called
  1. 零成本抽象的 Const 枚舉

常數枚舉在編譯期間會被完全刪除,並替換為它們的值。沒有運轉時開銷。

const enum LogLevel {
  Debug = 0,
  Info = 1,
  Warn = 2,
  Error = 3
}
// LogLevel.Debug becomes just 0 in the compiled code
  1. 組合的交叉類型

將多個類型合併為一個。對於合併不相關的類型,比 extends 更靈活。

type Timestamped = { createdAt: Date; updatedAt: Date }
type Authored = { authorId: string }
type Post = { title: string; content: string } & Timestamped & Authored
  1. NoInfer 實用程式類型

TypeScript 5.4 中的新功能NoInfer可防止在特定位置進行類型推斷。這對於強制顯式類型參數很有用。這有助於避免基於錯誤的參數意外擴展類型。

// Without NoInfer - T gets inferred from both parameters
function createState<T>(initial: T, actions: T) {
  return { state: initial, actions }
}

// Problem: T becomes string | number instead of just string
const state1 = createState('hello', 42) // Oops, mixed types

// With NoInfer - T only inferred from initial
function createStateSafe<T>(initial: T, actions: NoInfer<T>) {
  return { state: initial, actions }
}

// Now this errors as expected
const state2 = createStateSafe('hello', 42) // ✗ Error

// Must explicitly specify T to allow different types
const state3 = createStateSafe<string | number>('hello', 42) // ✓

總結

這些技巧讓我節省了無數的偵錯時間,也讓 UserJot 程式碼庫更容易維護。 TypeScript 功能強大,但你不需要用到所有功能。選擇那些能夠解決你程式碼中實際問題的功能。先從幾個開始,熟悉之後,再逐步增加更多功能。

如果您正在建立產品,並且需要收集使用者回饋、管理路線圖或透過更新日誌向使用者提供最新訊息,不妨試試UserJot 。我一直在用 TypeScript 技巧建立它,它旨在幫助團隊建立用戶真正想要的產品。此外,它的免費套餐非常慷慨,您可以直接試用,無需購買任何產品。

UserJot 回饋板,包含使用者討論


原文出處:https://dev.to/shayy/20-typescript-tricks-every-developer-should-know-94c


共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。

阿川私房教材:
學 JavaScript 前端,帶作品集去面試!

63 個專案實戰,寫出作品集,讓面試官眼前一亮!

立即開始免費試讀!