過去幾個月,我一直在建立UserJot ,一個面向 SaaS 團隊的回饋和路線圖平台。這個專案讓我對 TypeScript 有了深入的了解——在此之前,我偶爾會使用它,但從未達到如此大規模。經過長時間的 TypeScript 生產程式碼編寫、類型錯誤除錯以及為了提高類型安全性而進行的重構,我掌握了一些技巧,這些技巧確實能加快開發速度,並在 bug 進入生產環境之前就將其捕獲。
這些並非教科書上的理論模式,而是我每天都在使用的實用技巧,都是在實際解決問題的過程中發現的。有些技巧可以幫助我避免執行時錯誤,有些則能讓程式碼更簡潔、更易用。以下是 20 個真正有用的技巧。
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
為物件新增as const
,TypeScript 會將所有屬性視為唯讀,並推斷出盡可能具體的類型。這對於不應更改的配置資料非常有用。
const routes = {
home: '/',
dashboard: '/dashboard',
settings: '/settings'
} as const
// routes.home is '/' not string
模板字面量類型可讓您建立與特定字串模式相符的類型。非常適合 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
使用通用屬性來區分聯合成員。 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
}
}
建立函數來告訴 TypeScript 某個物件是什麼類型。這比散佈在程式碼中的多個 typeof 檢查要簡潔得多。
function isString(value: unknown): value is string {
return typeof value === 'string'
}
使用括號表示法從其他類型中提取類型。這可以保持類型的 DRY 原則,並在來源類型變更時自動更新。
type User = { id: string; name: string; email: string }
type UserEmail = User['email'] // string
type UserKeys = keyof User // 'id' | 'name' | 'email'
使用條件類型來建立根據條件變化的類型。可以將它們視為類型的三元運算子。
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 }
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'>
為不同的用例提供多個函數簽名。這能為函數的使用者提供更好的自動補全和類型檢查功能。
// 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
限制可以傳遞給泛型的類型。這可以避免錯誤並提供更好的 IntelliSense。
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
系統地轉換一個類型的所有屬性。非常適合建立現有類型的變體。
// 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] }
使用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}`)
}
}
根據需要擴充現有模組類型。適用於新增屬性或擴充第三方程式庫。
declare global {
interface Window {
analytics: AnalyticsClient
}
}
使用僅類型導入來確保導入在執行時被移除。這可以減少套件的大小並防止循環依賴。
import type { User } from './types'
import { type Config, validateConfig } from './config'
建立斷言條件和縮小類型的函數。如果斷言失敗,函數將拋出異常。這對於在執行時驗證資料並同時保證 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')
}
}
建立結構相同但名義不同的類型。避免混淆相似的原始型別。這種模式讓我避免了許多傳遞錯誤 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'
使用泛型追蹤建構器的狀態,並確保方法以正確的順序呼叫。此模式可確保方法鏈的編譯時安全性。
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
常數枚舉在編譯期間會被完全刪除,並替換為它們的值。沒有運轉時開銷。
const enum LogLevel {
Debug = 0,
Info = 1,
Warn = 2,
Error = 3
}
// LogLevel.Debug becomes just 0 in the compiled code
將多個類型合併為一個。對於合併不相關的類型,比 extends 更靈活。
type Timestamped = { createdAt: Date; updatedAt: Date }
type Authored = { authorId: string }
type Post = { title: string; content: string } & Timestamped & Authored
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 技巧建立它,它旨在幫助團隊建立用戶真正想要的產品。此外,它的免費套餐非常慷慨,您可以直接試用,無需購買任何產品。
原文出處:https://dev.to/shayy/20-typescript-tricks-every-developer-should-know-94c