使用條件類型與 infer 在 TypeScript 中構建類型化 fetch
最近,我在處理基于 OpenAPI 模式生成的 API 請求和響應類型時,發現可以利用 TypeScript 的條件類型, 讓 fetch 邏輯能根據調用的路徑和使用的 HTTP 方法自動推斷出合適的參數和響應類型,這確實非常酷。我查閱 了文檔想了解更多,卻發現文檔對這種類型推斷的介紹并不深入,因此決定分享這個實用技巧。
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return ? Return : never;本文將展示 TypeScript 如何通過extends和infer關鍵字從復雜嵌套結構中智能提取類型。
前置知識
本文假設您已了解TypeScript 泛型的基本概念。
條件類型:編譯器的決策機制
條件類型就像是 TypeScript 中的 if 語句:"如果滿足這個條件,就是「這種」類型,否則是「那種」類型"。其語法類似 JavaScript 的三元運算符,特別適用于需要根據輸入返回不同類型的函數:
type myFn = (arg: SomeType | OtherType) => arg extends SomeType ? string : number;extends在類型聲明中不是陳述而是提問:"schema[P][M]是否擴展了這個對象?"如果條件為真,返回?后的類型;否則返回:后的類型。
infer 關鍵字:類型提取利器
雖然TypeScript 文檔對infer的介紹不多,但它能優雅地提取函數返回類型:
type GetReturnType<Type> = Type extends (...args: never[]) => infer Return ? Return : never;
type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<(x: string) => string>; // string
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>; // boolean[]這個關鍵字相當于告訴編譯器:"你告訴我這是什么類型,我不想全部寫出來"。在處理具有相同屬性不同組合的多種類型時特別有用。
調試技巧
由于 TypeScript 缺少類型調試器,我通常通過 VSCode的IntelliSense來檢查類型:
type MyComplexType<T,Q> = {....}
type test = MyComplexType<"test value", "another test value">示例項目:鳥類觀測網站
我們正在構建一個鳥類觀測網站,需要確保 API 請求包含正確參數,并以類型安全的方式使用返回數據。以下是簡化的 API 模式:
export interface schema {
'/birds': {
get: {
parameters: {
query?: {
type?: string; // 按類型篩選
habitat?: string; // 按棲息地篩選
colour?: string; // 按顏色篩選
};
};
response: {
content: {
id?: number;
name?: string; // 如"Avocet"
type?: string; // 如"wader"
habitats?: string[]; // 如["lakes","wetlands"]
colours?: string[];
distinctiveFeatures?: string;
wingspan?: number; // 翼展(厘米)
image?: string; // 圖片URL
};
};
};
};
'/users': {
post: {
requestBody?: {
content: {
name?: string; // 用戶名
email?: string; // 郵箱
favouriteBird?: number[]; // 收藏的鳥類ID
};
};
response: {
content: {
id?: number;
name?: string; // 如"Billie Sandpiper"
favouriteBirds?: number[]; // 如[12,14]
email?: string; // 如"billie@example.com"
};
};
};
};
}原生 fetch 的局限性
原生fetch無法利用我們定義的類型:
const rsp = await fetch('https://api.example.com/bird/12');
const data = await rsp.json(); // 此時不知道具體類型而且fetch不是泛型函數,不能直接指定請求和響應類型:
await fetch<GetBirdRequest, GetBirdResponse>(...) // 這樣不行雖然可以手動指定返回類型,但很繁瑣:
const data = (await rsp.json()) as schema['/bird/{birdId}']['get']['response']['content'];構建 createFetcher 函數
我們將創建一個createFetcher高階函數,根據路徑和 HTTP 方法返回知道具體參數和返回類型的函數:
function createFetcher(path, method) {
return async (params) => { // ... }
}
const getBird = createFetcher('/birds/{birdId}', 'get')
const data = await getBird({ path: { birdId: 12 } })參數錯誤時會得到明確的類型提示:
const data = await getBird({});
// 錯誤:缺少必需的path參數實現細節
- 基礎結構:
import { schema } from'./api';
function createFetcher<P extends keyof schema, M extends keyof schema[P]>(path: P, method: M) {
returnasync (params) => {
const baseUrl = 'https://api.example.com';
const fetchUrl = newURL(path, baseUrl);
constoptions: RequestInit = { method: method asstring };
const data = awaitfetch(fetchUrl, options);
returnawait data.json();
};
}- 參數類型推斷:
type Params<P extends keyof schema, M extends keyof schema[P]> = schema[P][M] extends {
parameters: {
query?: infer Q;
path?: infer PP;
requestBody?: { content: infer RB };
};
}
? {
query?: Q extendsundefined ? never : Q;
path: PP;
requestBody?: RBextendsundefined ? never : RB;
}
: never;- 路徑參數替換:
const pathParams = path.match(/{([^}]+)}/g);
let realPath = path as string;
pathParams?.forEach((param) => {
const paramName = param.replace(/{|}/g, '');
realPath = realPath.replace(param, params?.path?.[paramName]);
});- 處理查詢參數和請求體:
if (params?.query) {
Object.entries(params.query).forEach(([key, value]) => {
fetchUrl.searchParams.append(key, value as string);
});
}
if (params?.requestBody) {
options.body = JSON.stringify(params.requestBody);
options.headers = { 'Content-Type': 'application/json' };
}- 響應類型處理:
type ResponseT<P extends keyof schema, M extends keyof schema[P]> = schema[P][M] extends {
response: { content: infer R };
}
? R
: never;
return fetch(fetchUrl, options).then((res) => res.json() as ResponseT<P, M>);最終成果
const listBirds = createFetcher('/birds', 'get');
const allBirds = awaitlistBirds();
const getBird = createFetcher('/birds/{birdId}', 'get');
const bird = awaitgetBird({ path: { birdId: 12 } });
const addSighting = createFetcher('/users/{userId}/sightings', 'post');
const mySighting = awaitaddSighting({
path: { userId: 1 },
requestBody: {
birdId: 226,
timestamp: '2025-06-04T13:00:00Z',
lat: 51.4870924,
long: 0.2228486,
notes: '在樹上聽到它的歌聲!',
},
});通過條件類型和infer,我們成功創建了完全類型化的 fetch 函數,使 API 調用更加安全和便捷。
原文地 址:https://piccalil.li/blog/building-a-typed-fetch-in-typescript-with-conditional-types-and-infer/作者:Sophie Koonin





























