-
Notifications
You must be signed in to change notification settings - Fork 13.1k
Description
🔎 Search Terms
Readonly generic parameter conditional type inference conditional type matching with Readonly parameter generic constraint Readonly intersection type mapped type readonly conditional inference failure Readonly parameter affects return type inference
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about conditional types,
generic inference, mapped types, and readonly
⏯ Playground Link
💻 Code
// ============ Setup code ==================
type TypeFunction<ReturnType = unknown> = (...args: any[]) => ReturnType;
type Flags = {
[flagName: string]: {
type: TypeFunction;
default?: unknown;
};
};
type TypeFlag<Schemas extends Flags> = {
[flag in keyof Schemas]: Schemas[flag] extends { type: TypeFunction<infer T>; }
? T
: never
};
// ============ Works without using Readonly<> ==================
declare function fnWorking<Options extends Flags>(
options: Options
): TypeFlag<Options>;
const resultWorking = fnWorking({
booleanFlag: { type: Boolean },
booleanFlagDefault: {
type: Boolean,
default: false,
},
});
resultWorking.booleanFlag
// ^? (property) booleanFlag: boolean
resultWorking.booleanFlagDefault
// ^? (property) booleanFlagDefault: boolean
// ============ Bug when using Readonly<> ==================
declare function fnBug<Options extends Flags>(
options: Readonly<Options>
): TypeFlag<Options>;
const resultBug = fnBug({
booleanFlag: { type: Boolean },
booleanFlagDefault: {
type: Boolean,
default: false,
},
});
resultBug.booleanFlag
// ^? (property) booleanFlag: boolean
resultBug.booleanFlagDefault
// ^? (property) booleanFlagDefault: never🙁 Actual behavior
When a generic function has Readonly<T> in the parameter position, conditional type matching in the return type fails even though T itself is not readonly:
declare function fnBug<Options extends Flags>(
options: Readonly<Options>
): TypeFlag<Options>; // Options is NOT readonly here
type Test = typeof resultBug. booleanFlagDefault; // Returns `never` (incorrect)The conditional type Schemas[flag] extends { type: TypeFunction<infer T> } fails to match and falls through to the never branch.
This happens even though:
Optionsitself is notReadonlyin the return typeTypeFlag<Options>- The inferred type from the argument has the correct structure
- The same pattern works perfectly without
Readonly<Options>in the parameter
🙂 Expected behavior
The conditional type should match successfully regardless of whether the parameter is Readonly<Options> or Options, since the return type TypeFlag<Options> uses Options directly (not Readonly<Options>).
Expected result:
type Test = typeof resultBug. booleanFlagDefault; // Should be `boolean`The Readonly modifier in the parameter position should not affect type inference in the return type when the generic parameter Options itself is not readonly.
Additional information about the issue
Structural typing inconsistency
The types are structurally compatible:
{ type: BooleanConstructor; default: false } should match { type: TypeFunction<infer T> }
but TypeScript's conditional type matching fails when Readonly<Options> appears in the parameter.
Workaround exists
Adding an intersection to the conditional pattern fixes the issue, proving this is a pattern-matching bug rather than a semantic issue:
Schemas[flag] extends ({ type: TypeFunction<infer T> } & Record<PropertyKey, unknown>)This workaround doesn't change the logical meaning but helps TypeScript's matcher recognize the types as compatible.
Real world Impact
This bug affects any library that wants to accept readonly parameters while using conditional type inference, forcing authors to use syntactic workarounds that pollute the type definitions.