TypeScript条件类型:区分索引类型与对象类型的技术问询
I get it, dealing with overlapping TypeScript conditional types can be tricky—especially when trying to differentiate between objects with index signatures and those with statically defined keys. Let's break down why your original code wasn't working and fix it step by step.
Why Your Original Approach Failed
In TypeScript, any object type (including those with static keys) is assignable to object, and types with index signatures are also a subset of object. Since conditional types evaluate top-to-bottom, your commented-out MapSchema branch was never reached—because all objects (including index signature ones) would hit the T extends object branch first.
Fix: Use Key-Based Type Checks to Differentiate
To tell apart objects with only index signatures from static-key objects, we can leverage TypeScript's ability to inspect the keys of a type. Here's how to adjust your Schema type:
Step 1: Create a Helper Type to Detect Index Signatures
First, define a conditional type that checks if a type's keys are exclusively indexable (i.e., string, number, or symbol, with no static named keys):
type IsPureIndexSignature<T> = // Check if all keys are either string, number, or symbol keyof T extends string | number | symbol // Ensure there are no non-indexable keys left ? Exclude<keyof T, string | number | symbol> extends never ? true : false : false;
Step 2: Update Your Schema Type
Now, restructure your Schema type to first handle index signature objects before falling back to static objects. We'll nest the object checks to prioritize the index signature case:
// Assume these are your existing schema types type StringSchema<R> = { type: "string"; transform?: (val: string) => R }; type NumberSchema = { type: "number" }; type DateSchema = { type: "date" }; type BooleanSchema = { type: "boolean" }; type ArraySchema<E, R> = { type: "array"; elementSchema: Schema<E, R> }; type TupleSchema<U> = { type: "tuple"; firstElementSchema: Schema<U> }; type MapSchema<V, R> = { type: "map"; valueSchema: Schema<V, R> }; type ObjectSchema<T, R> = { type: "object"; properties: { [K in keyof T]: Schema<T[K], R extends object ? R[K] : T[K]> } }; export type Schema<T, R = T> = T extends string ? StringSchema<R> : T extends number ? NumberSchema : T extends Date ? DateSchema : T extends boolean ? BooleanSchema : T extends Array<infer E> ? ArraySchema<E, R> : T extends [infer U, ...unknown[]] ? TupleSchema<U> : T extends object ? IsPureIndexSignature<T> extends true // Extract the value type from the index signature ? MapSchema<Extract<T[keyof T], unknown>, R> : ObjectSchema<T, R> : never;
How This Works
- Pure Index Signature Objects: For types like
{[key: string]: number},keyof Tisstring, soIsPureIndexSignature<T>returnstrue, and we useMapSchema. - Static Key Objects: For types like
{ name: string; age: number },keyof Tis"name" | "age"—Exclude<keyof T, string | number | symbol>leaves"name" | "age"(since those are literal strings, not thestringtype itself), soIsPureIndexSignature<T>returnsfalse, and we useObjectSchema. - Mixed Types (Static Keys + Index Signature): For types like
{ id: string; [key: string]: unknown },keyof Tis"id" | string.Excludeleaves"id", soIsPureIndexSignature<T>returnsfalse, and we default toObjectSchema. If you want to treat mixed types asMapSchema, you can adjust the helper type to check if an index signature exists (instead of checking for pure index signatures).
Example Usage
Let's test this with different types:
// Pure index signature -> MapSchema type DictSchema = Schema<{[key: string]: number}>; // Type: { type: "map"; valueSchema: { type: "number" } } // Static object -> ObjectSchema type UserSchema = Schema<{ name: string; age: number }>; // Type: { type: "object"; properties: { name: { type: "string"; transform?: (val: string) => string; }, age: { type: "number" } } } // Tuple -> TupleSchema type TupleSchemaTest = Schema<[string, number]>; // Type: { type: "tuple"; firstElementSchema: { type: "string"; transform?: (val: string) => string; } }
Edge Cases to Consider
- Empty Object
{}:keyof {}isnever, soIsPureIndexSignature<T>returnsfalse, and it will useObjectSchema. This is probably what you want, since an empty object has no index signature or static keys. - Readonly Index Signatures: The helper type works with
readonly [key: string]: Vas well, sincekeyofincludes readonly keys.
内容的提问来源于stack exchange,提问作者sowdri

