TypeScript 5.8パターンマッチング:7つの高度な型レベルプログラミングパターン
前端工程
型プログラミングの4つのペインポイント
string | number | booleanというユニオン型を定義したのに、各分岐を手動で処理しなければならない。条件型が3層にネストして同僚が首を横に振る。ReturnType<typeof fn>がanyに推論される。実行時のデータと型定義が一致しない。TypeScriptの型システムはチューリング完全なのに、あなたはPartialとPickしか知らない。 ユニオン型が広すぎる、条件型のネストが読めない、型推論が不正確、実行時型チェックが不在——この4つのペインポイントにより、ほとんどの開発者の型プログラミングは「コンパイルさえ通ればいい」レベルに留まっている。
本記事ではコア概念から出発し、条件型とinfer→テンプレートリテラル型→再帰型→マップ型→型レベル数学→実行時型安全→型レベルステートマシンの7つの高度なパターンを、型推論から実行時検証まで一歩ずつ解説する。
コア概念
| 概念 | 説明 |
|---|---|
| 条件型 | T extends U ? X : Y、型の関係に基づいて異なる型分岐を選択、型レベルプログラミングの基盤 |
| 分配条件型 | Tがユニオン型の場合、条件型が各メンバーに対して自動的に評価され、型レベルマッピングを実現 |
| inferキーワード | 条件型で型変数を宣言し、推論対象の型から部分型を抽出、例:infer R |
| テンプレートリテラル型 | `prefix_${T}`、文字列リテラル型の結合とパターンマッチング |
| 再帰型 | 型定義内で自身を参照し、深い走査と複雑な型変換を実現、再帰深度制限の対象 |
| マップ型 | { [K in keyof T]: ... }、既存の型から新しい型を生成、型変換の中核ツール |
| 型ナローイング | typeof、in、instanceofなどで制御フロー内の型範囲を絞り込む |
| satisfies演算子 | expr satisfies Type、式が型を満たすことを検証しつつ推論型を広げない、TypeScript 4.9+ |
型レベルプログラミングフロー
型推論:
条件型(extends) → infer抽出 → 分配評価 → 型ナローイング
型変換:
マップ型(keyof) → テンプレートリテラル(結合/マッチング) → 再帰走査(深い変換) → 型合成
型安全:
satisfies検証 → Zod実行時検証 → 型レベルステートマシン → コンパイル時+実行時の一貫性
問題分析:型レベルプログラミングの5つの課題
- 型の可読性が低い:ネストされた条件型と再帰型は理解が困難。
type X = T extends A ? B extends C ? D : E : Fの3層ネストは圧倒的。型レベルのコメントや中間名称が不足 - コンパイルが遅い:複雑な再帰型と大型ユニオン型のインスタンス化がコンパイラリソースを大量消費。10層の再帰型で
tscが5倍遅くなる - 型と実行時の不一致:TypeScriptの型は実行時に消去される。コンパイル時の正確な型定義が実行時のデータ安全を保証できない。APIレスポンスと型定義が乖離
- 再帰深度制限:TypeScriptの再帰型はデフォルトで約1000層の深度制限。複雑な型変換で
Type instantiation is excessively deepが発生しやすい - デバッグが困難:型エラーメッセージが冗長で読みにくい。
inferの推論失敗時に中間状態を確認できない。複雑な型変換のデバッグ手段がほぼ皆無
ステップバイステップ:7つの高度なパターン
パターン1:条件型とinferパターンマッチング
type MatchResult<T, Pattern> = T extends Pattern ? { matched: true; value: T } : { matched: false };
type ExtractPromiseValue<T> = T extends Promise<infer V> ? V : never;
type ExtractArrayElement<T> = T extends (infer E)[] ? E : never;
type ExtractFunctionReturn<T> = T extends (...args: any[]) => infer R ? R : never;
type Unwrap<T> = T extends Promise<infer V>
? V extends Promise<infer V2>
? V2
: V
: T;
type DeepUnwrap<T> = T extends Promise<infer V> ? DeepUnwrap<V> : T;
type ParseRoute<S extends string> =
S extends `${string}:${infer Param}/${infer Rest}`
? Param | ParseRoute<Rest>
: S extends `${string}:${infer Param}`
? Param
: never;
type RouteParams = ParseRoute<'/api/users/:userId/posts/:postId'>;
type ExtractConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;
type EventMap = {
click: { x: number; y: number };
focus: { element: string };
keydown: { key: string; code: string };
};
type ExtractEventData<T extends keyof EventMap> = EventMap[T];
type InferTuple<T extends any[]> = T extends [infer First, ...infer Rest]
? [First, ...InferTuple<Rest>]
: [];
type Swap<T extends any[]> = T extends [infer A, infer B] ? [B, A] : T;
パターン2:テンプレートリテラル型パターンマッチング
type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S;
type TrimRight<S extends string> = S extends `${infer Rest} ` ? TrimRight<Rest> : S;
type Trim<S extends string> = TrimLeft<TrimRight<S>>;
type CapitalizeWords<S extends string> =
S extends `${infer First} ${infer Rest}`
? `${Capitalize<First>} ${CapitalizeWords<Rest>}`
: Capitalize<S>;
type KebabToCamel<S extends string> =
S extends `${infer First}-${infer Rest}`
? `${First}${Capitalize<KebabToCamel<Rest>>}`
: S;
type CamelToKebab<S extends string> =
S extends `${infer First}${infer Rest}`
? First extends Uppercase<First>
? `${Lowercase<First>}${CamelToKebab<Rest>}`
: `-${Lowercase<First>}${CamelToKebab<Rest>}`
: S;
type ParseUrlQuery<S extends string> =
S extends `${infer Key}=${infer Value}&${infer Rest}`
? { [K in Key | keyof ParseUrlQuery<Rest>]: K extends Key ? Value : ParseUrlQuery<Rest>[K & keyof ParseUrlQuery<Rest>] }
: S extends `${infer Key}=${infer Value}`
? { [K in Key]: Value }
: {};
type CSSProperty = 'margin-top' | 'margin-bottom' | 'padding-left' | 'padding-right';
type CSSToJSStyle<S extends string> = KebabToCamel<S>;
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type ApiEndpoint = `/api/${string}`;
type RoutePattern = `${HttpMethod} ${ApiEndpoint}`;
type ParseHTTPHeader<S extends string> =
S extends `${infer Name}: ${infer Value}`
? { name: Name; value: Value }
: never;
type VersionString = `${number}.${number}.${number}`;
type ParseVersion<S extends string> =
S extends `${infer Major}.${infer Minor}.${infer Patch}`
? { major: Major; minor: Minor; patch: Patch }
: never;
パターン3:再帰型と深い走査
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
type DeepRequired<T> = T extends object
? { [K in keyof T]-?: DeepRequired<T[K]> }
: T;
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
type DeepMutable<T> = T extends object
? { -readonly [K in keyof T]: DeepMutable<T[K]> }
: T;
type PathKeys<T, Prefix extends string = ''> = T extends object
? { [K in keyof T & string]: PathKeys<T[K], Prefix extends '' ? K : `${Prefix}.${K}`> }[keyof T & string]
: Prefix;
type GetValueByPath<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetValueByPath<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
type FlattenObject<T, Prefix extends string = ''> = T extends object
? { [K in keyof T & string]: FlattenObject<T[K], Prefix extends '' ? K : `${Prefix}.${K}`> }[keyof T & string] extends infer U
? { [K in U]: U extends `${infer P}.${infer R}` ? never : U }
: never
: { [K in Prefix]: T };
type DeepPick<T, Paths extends readonly string[]> =
Paths extends [infer First, ...infer Rest]
? First extends keyof T
? Rest extends string[]
? DeepPick<T[First], Rest> & { [K in First]: T[First] }
: { [K in First]: T[First] }
: {}
: {};
type RecursiveOmit<T, K extends string> = T extends object
? { [P in keyof T as P extends K ? never : P]: RecursiveOmit<T[P], K> }
: T;
type Accordion<S extends any[], T extends any[] = []> =
T['length'] extends 10
? T
: S extends [infer First, ...infer Rest]
? Accordion<Rest, [...T, First]>
: T;
パターン4:マップ型の高度な変換
type OptionalKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? K : never }[keyof T];
type RequiredKeys<T> = { [K in keyof T]-?: {} extends Pick<T, K> ? never : K }[keyof T];
type PickByValue<T, ValueType> = { [K in keyof T as T[K] extends ValueType ? K : never]: T[K] };
type OmitByValue<T, ValueType> = { [K in keyof T as T[K] extends ValueType ? never : K]: T[K] };
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type MakeRequired<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
type MutableKeys<T> = { [K in keyof T]-?: Equal<{ -readonly [P in K]: T[P] }, { [P in K]: T[P] }> extends true ? K : never }[keyof T];
type ReadonlyKeys<T> = { [K in keyof T]-?: Equal<{ -readonly [P in K]: T[P] }, { [P in K]: T[P] }> extends true ? never : K }[keyof T];
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type BrandKeys<T, B extends string> = { [K in keyof T]: T[K] & { __brand: B } };
type PropType<T, Path extends string> =
Path extends `${infer K}.${infer Rest}`
? K extends keyof T ? PropType<T[K], Rest> : never
: Path extends keyof T ? T[Path] : never;
interface APIResponse {
user: {
profile: { name: string; avatar: string };
settings: { theme: 'dark' | 'light'; lang: string };
};
meta: { page: number; total: number };
}
type UserName = PropType<APIResponse, 'user.profile.name'>;
type Theme = PropType<APIResponse, 'user.settings.theme'>;
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
type TupleToUnion<T extends any[]> = T[number];
type StringKeyOf<T> = keyof T & string;
type MarkRequired<T, RK extends keyof T> = Omit<T, RK> & Required<Pick<T, RK>>;
パターン5:型レベル数学演算
type BuildTuple<N extends number, T extends any[] = []> =
T['length'] extends N ? T : BuildTuple<N, [...T, unknown]>;
type Add<A extends number, B extends number> = [...BuildTuple<A>, ...BuildTuple<B>]['length'];
type Subtract<A extends number, B extends number> =
BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest] ? Rest['length'] : never;
type Multiply<A extends number, B extends number, Acc extends any[] = []> =
B extends 0 ? Acc['length']
: Multiply<A, Subtract<B, 1>, [...Acc, ...BuildTuple<A>]>;
type Compare<A extends number, B extends number> =
A extends B ? 0
: BuildTuple<A> extends [...BuildTuple<B>, ...any[]] ? 1 : -1;
type IsGreaterThan<A extends number, B extends number> = Compare<A, B> extends 1 ? true : false;
type Fibonacci<N extends number> =
N extends 0 ? 0
: N extends 1 ? 1
: Add<Fibonacci<Subtract<N, 1>>, Fibonacci<Subtract<N, 2>>>;
type Fib10 = Fibonacci<10>;
type Range<From extends number, To extends number, Acc extends number[] = []> =
From extends To ? [...Acc, From]
: Range<Add<From, 1>, To, [...Acc, From]>;
type OneToFive = Range<1, 5>;
type Divide<A extends number, B extends number, Quotient extends any[] = [], Remainder extends any[] = BuildTuple<A>> =
B extends 0 ? never
: Remainder extends [...BuildTuple<B>, ...infer NewRemainder]
? Divide<A, B, [...Quotient, unknown], NewRemainder>
: Quotient['length'];
type Mod<A extends number, B extends number> =
B extends 0 ? never
: BuildTuple<A> extends [...BuildTuple<B>, ...infer Rest]
? Mod<Rest['length'], B>
: A;
type IsEven<N extends number> = Mod<N, 2> extends 0 ? true : false;
パターン6:実行時型安全(Zod統合)
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email(),
age: z.number().int().min(0).max(150),
role: z.enum(['admin', 'editor', 'viewer']),
tags: z.array(z.string()).default([]),
metadata: z.record(z.unknown()).optional(),
});
type User = z.infer<typeof UserSchema>;
type UserInput = z.input<typeof UserSchema>;
const CreateUserSchema = UserSchema.omit({ id: true });
type CreateUserInput = z.infer<typeof CreateUserSchema>;
const PatchUserSchema = UserSchema.partial().omit({ id: true });
type PatchUserInput = z.infer<typeof PatchUserSchema>;
function validateOrThrow<T>(schema: z.ZodSchema<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
throw new ValidationError(result.error);
}
return result.data;
}
class ValidationError extends Error {
constructor(public readonly zodError: z.ZodError) {
super(zodError.message);
this.name = 'ValidationError';
}
get fieldErrors(): Record<string, string[]> {
return this.zodError.flatten().fieldErrors as Record<string, string[]>;
}
}
function createTypeSafeFetcher<T>(schema: z.ZodSchema<T>) {
return async (url: string, options?: RequestInit): Promise<T> => {
const response = await fetch(url, options);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const raw = await response.json();
return validateOrThrow(schema, raw);
};
}
const fetchUser = createTypeSafeFetcher(UserSchema);
const ApiRoutesSchema = z.union([
z.object({ method: z.literal('GET'), path: z.string(), response: z.any() }),
z.object({ method: z.literal('POST'), path: z.string(), body: z.any(), response: z.any() }),
z.object({ method: z.literal('PUT'), path: z.string(), body: z.any(), response: z.any() }),
z.object({ method: z.literal('DELETE'), path: z.string(), response: z.any() }),
]);
const EventPayloadSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('click'), x: z.number(), y: z.number() }),
z.object({ type: z.literal('focus'), element: z.string() }),
z.object({ type: z.literal('keydown'), key: z.string(), code: z.string() }),
]);
type EventPayload = z.infer<typeof EventPayloadSchema>;
パターン7:型レベルステートマシン
interface StateMachineDef {
states: Record<string, Record<string, string>>;
}
type ValidateStateMachine<Def extends StateMachineDef> = {
[State in keyof Def['states']]: {
[Event in keyof Def['states'][State]]: Def['states'][State][Event] extends keyof Def['states']
? Def['states'][State][Event]
: never;
};
};
type Transition<CurrentState, Event, Def extends StateMachineDef> =
CurrentState extends keyof Def['states']
? Event extends keyof Def['states'][CurrentState]
? Def['states'][CurrentState][Event]
: CurrentState
: never;
type ReachableStates<From extends string, Def extends StateMachineDef, Visited extends string = never> =
From extends Visited
? never
: From | Def['states'][From] extends infer Transitions
? Transitions extends Record<string, string>
? ReachableStates<Transitions[keyof Transitions], Def, Visited | From>
: never
: never;
interface OrderStateMachine extends StateMachineDef {
states: {
draft: { submit: 'pending_review'; discard: 'cancelled' };
pending_review: { approve: 'approved'; reject: 'rejected'; request_changes: 'draft' };
approved: { start: 'in_progress'; cancel: 'cancelled' };
in_progress: { complete: 'completed'; hold: 'on_hold'; cancel: 'cancelled' };
on_hold: { resume: 'in_progress'; cancel: 'cancelled' };
completed: {};
cancelled: {};
rejected: {};
};
}
type OrderState = keyof OrderStateMachine['states'];
type OrderEvent = keyof OrderStateMachine['states'][keyof OrderStateMachine['states']];
type AfterSubmit = Transition<'draft', 'submit', OrderStateMachine>;
type AfterApprove = Transition<'pending_review', 'approve', OrderStateMachine>;
class TypeSafeStateMachine<Def extends StateMachineDef> {
private current: keyof Def['states'];
constructor(
private readonly definition: Def,
initialState: keyof Def['states'],
) {
this.current = initialState;
}
send<E extends string>(
event: E & (E extends keyof Def['states'][typeof this.current] ? E : never),
): Transition<typeof this.current, E, Def> {
const stateTransitions = this.definition.states[this.current as string] as Record<string, string>;
const nextState = stateTransitions[event];
if (!nextState) {
throw new Error(`Invalid event "${event}" in state "${String(this.current)}"`);
}
this.current = nextState as keyof Def['states'];
return nextState as Transition<typeof this.current, E, Def>;
}
getState(): keyof Def['states'] {
return this.current;
}
canSend<E extends string>(event: E): boolean {
const stateTransitions = this.definition.states[this.current as string] as Record<string, string>;
return event in stateTransitions;
}
}
const orderFsm = new TypeSafeStateMachine<OrderStateMachine>(
{ states: { draft: { submit: 'pending_review', discard: 'cancelled' }, pending_review: { approve: 'approved', reject: 'rejected', request_changes: 'draft' }, approved: { start: 'in_progress', cancel: 'cancelled' }, in_progress: { complete: 'completed', hold: 'on_hold', cancel: 'cancelled' }, on_hold: { resume: 'in_progress', cancel: 'cancelled' }, completed: {}, cancelled: {}, rejected: {} } },
'draft',
);
よくある落とし穴
落とし穴1:分配条件型の予期しない動作
// ❌ 裸の型パラメータが分配をトリガーし、ユニオン型が展開される
type Wrap<T> = T extends any ? { value: T } : never;
type Result = Wrap<string | number>; // { value: string } | { value: number }
// ✅ タプルでラップして分配を防止
type WrapNoDistribute<T> = [T] extends [any] ? { value: T } : never;
type ResultNoDistribute = WrapNoDistribute<string | number>; // { value: string | number }
落とし穴2:再帰型の深度オーバーフロー
// ❌ 無限再帰でコンパイラがクラッシュ
type DeepFlatten<T> = T extends Array<infer U> ? DeepFlatten<U> : T;
// ✅ 再帰深度を制限
type FlattenN<T, N extends number, D extends any[] = []> =
D['length'] extends N ? T
: T extends Array<infer U> ? FlattenN<U, N, [...D, 1]>
: T;
type SafeFlatten<T> = FlattenN<T, 5>;
落とし穴3:条件型以外でのinferの使用
// ❌ inferは条件型のextends節でのみ使用可能
type BadExtract<T> = T extends Promise<infer V> ? V : infer V;
// ✅ すべてのinferはextends節で宣言する
type GoodExtract<T> = T extends Promise<infer V> ? V : T;
落とし穴4:satisfiesと型アサーションの混同
// ❌ as constは型検証を失う
const config = { port: 3000, host: 'localhost' } as const;
// ✅ satisfiesは型を検証しつつリテラル型を保持
const config = { port: 3000, host: 'localhost' } as const satisfies Record<string, string | number>;
落とし穴5:テンプレートリテラル型のパフォーマンストラップ
// ❌ 広すぎるテンプレートリテラルでコンパイラが大量の組み合わせを試行
type BadRoute = `${string}/${string}/${string}/${string}`;
// ✅ 具体的なユニオン型でテンプレートリテラルを制約
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type GoodRoute = `/api/${ApiVersion}/${Resource}`;
エラートラブルシューティング
| エラー | 考えられる原因 | 解決策 |
|---|---|---|
Type instantiation is excessively deep |
再帰型が深度制限を超過 | カウンタタプルで再帰深度を制限 |
Type produces a tuple type that is too large |
BuildTupleが過大なタプルを生成 | 数学演算の範囲を制限、大きな数の演算を避ける |
Expression produces a union type that is too complex |
テンプレートリテラルの組み合わせ爆発 | ${string}の代わりに具体的なユニオン型を使用 |
Type provides no match for the default export |
inferの推論失敗 | 条件型のextendsパターンが一致するか確認 |
Circularity detected |
型の循環参照 | 中間型エイリアスで循環を解消 |
Type 'X' is not assignable to type 'Y' |
分配条件型の予期しない展開 | [T] extends [U]で分配を防止 |
infer declarations can only be used in extends |
inferの誤った位置 | inferは条件型のextends節のみで使用 |
satisfies式の型不一致 |
値が型制約と一致しない | satisfiesの右辺型が実際の値と互換か確認 |
Cannot find name 'T' |
ジェネリックスコープエラー | ジェネリックパラメータが正しい型エイリアスや関数シグネチャで宣言されているか確認 |
Excessive stack depth comparing types |
複雑な型比較でスタックオーバーフロー | 型構造を簡略化、中間型エイリアスを使用 |
高度な最適化
型キャッシュとパフォーマンス
type CachedDeepPartial<T> = T extends object
? { [K in keyof T]?: CachedDeepPartial<T[K]> }
: T;
type Intermediate<A, B> = A extends B ? A : never;
type CachedResult<T> = Intermediate<T, string | number>;
型レベルユニットテスト
type Assert<T extends true> = T;
type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
type TestTrim = Assert<Equal<Trim<' hello '>, 'hello'>>;
type TestKebabToCamel = Assert<Equal<KebabToCamel<'my-component-name'>, 'myComponentName'>>;
type TestAdd = Assert<Equal<Add<3, 5>, 8>>;
type TestDeepPartial = Assert<Equal<keyof DeepPartial<{ a: string; b: number }>, 'a' | 'b'>>;
条件型の可読性
type IsString<T> = T extends string ? true : false;
type IsNumber<T> = T extends number ? true : false;
type IsPrimitive<T> = IsString<T> extends true ? true : IsNumber<T> extends true ? true : false;
type DescribedConditional<T> =
T extends string ? { type: 'string'; value: T } :
T extends number ? { type: 'number'; value: T } :
T extends boolean ? { type: 'boolean'; value: T } :
{ type: 'unknown'; value: T };
型推論ヘルパーユーティリティ
type ShowType<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type Expand<T> = T extends infer U ? { [K in keyof U]: U[K] } : never;
type Prettify<T> = { [K in keyof T]: T[K] } & {};
比較分析
| 特徴 | TypeScript型システム | Haskell型システム | Rust型システム |
|---|---|---|---|
| チューリング完全 | はい(再帰型) | はい(型族) | いいえ |
| パターンマッチング | 条件型 + infer | 代数的データ型 + パターンマッチング | match式 |
| 型推論 | ローカル推論 | グローバルHindley-Milner | ローカル推論 |
| 高階型 | シミュレーション(HKTなし) | ネイティブサポート | サポートなし |
| 実行時安全性 | 外部ライブラリ必要(Zod) | ネイティブ保証 | ネイティブ保証 |
| 再帰深度 | ~1000層 | ハード制限なし | N/A |
| 学習曲線 | 中 | 高 | 中 |
| エコシステム成熟度 | ★★★★★ | ★★★ | ★★★★★ |
| 実用性 | ★★★★★ | ★★★ | ★★★★★ |
TypeScriptパターンマッチングは「型体操」の見せびらかしではなく、プロダクショングレードの型安全の礎だ。 条件型 + inferでコンパイル時に型を抽出・変換し、テンプレートリテラル型で文字列型もパターンマッチング可能にし、再帰型で深い走査を実現し、Zod統合でコンパイル時と実行時の型安全を一貫させ、型レベルステートマシンでビジネスロジックを型レベルで検証可能にする。型プログラミングの最高到達点は最も複雑な型を書くことではなく、型システムに最も多くのバグを捕まえさせることだ。
おすすめツール
- JSONフォーマッター — TypeScript設定とAPIレスポンスJSONをフォーマット、型定義の問題を素早く特定
- コードフォーマッター — TypeScriptコードをフォーマット、チームの型定義スタイルを統一
- cURL to Code — APIリクエストを型安全なTypeScript fetchコードに変換
ブラウザローカルツールを無料で試す →
#TypeScript 5.8#模式匹配#类型体操#条件类型#模板字面量类型#2026#前端工程