From 720a860394c5c96c71475f434aae12659305a253 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 3 Nov 2023 09:22:07 +0100 Subject: [PATCH 1/7] Default reverse mapped type inference to its constraint --- src/compiler/checker.ts | 2 +- ...MappedDefaultInferenceToConstraint.symbols | 86 +++++++++++++++++++ ...seMappedDefaultInferenceToConstraint.types | 81 +++++++++++++++++ ...verseMappedDefaultInferenceToConstraint.ts | 33 +++++++ 4 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols create mode 100644 tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types create mode 100644 tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3ac1038a4cdbe..074b7761437ab 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -24853,7 +24853,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const templateType = getTemplateTypeFromMappedType(target); const inference = createInferenceInfo(typeParameter); inferTypes([inference], sourceType, templateType); - return getTypeFromInference(inference) || unknownType; + return getTypeFromInference(inference) || getBaseConstraintOfType(typeParameter) || unknownType; } function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator { diff --git a/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols new file mode 100644 index 0000000000000..aa6078e058588 --- /dev/null +++ b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols @@ -0,0 +1,86 @@ +//// [tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts] //// + +=== reverseMappedDefaultInferenceToConstraint.ts === +// https://github.com/microsoft/TypeScript/issues/56241 + +interface ParameterizedObject { +>ParameterizedObject : Symbol(ParameterizedObject, Decl(reverseMappedDefaultInferenceToConstraint.ts, 0, 0)) + + type: string; +>type : Symbol(ParameterizedObject.type, Decl(reverseMappedDefaultInferenceToConstraint.ts, 2, 31)) + + params?: Record; +>params : Symbol(ParameterizedObject.params, Decl(reverseMappedDefaultInferenceToConstraint.ts, 3, 15)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +} + +declare function setup< +>setup : Symbol(setup, Decl(reverseMappedDefaultInferenceToConstraint.ts, 5, 1)) + + TContext, +>TContext : Symbol(TContext, Decl(reverseMappedDefaultInferenceToConstraint.ts, 7, 23)) + + TGuards extends Record, +>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>ParameterizedObject : Symbol(ParameterizedObject, Decl(reverseMappedDefaultInferenceToConstraint.ts, 0, 0)) + +>(_: { +>_ : Symbol(_, Decl(reverseMappedDefaultInferenceToConstraint.ts, 10, 2)) + + types: { +>types : Symbol(types, Decl(reverseMappedDefaultInferenceToConstraint.ts, 10, 6)) + + context: TContext; +>context : Symbol(context, Decl(reverseMappedDefaultInferenceToConstraint.ts, 11, 10)) +>TContext : Symbol(TContext, Decl(reverseMappedDefaultInferenceToConstraint.ts, 7, 23)) + + }; + guards: { +>guards : Symbol(guards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 13, 4)) + + [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; +>K : Symbol(K, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 5)) +>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11)) +>context : Symbol(context, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 27)) +>TContext : Symbol(TContext, Decl(reverseMappedDefaultInferenceToConstraint.ts, 7, 23)) +>params : Symbol(params, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 45)) +>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11)) +>K : Symbol(K, Decl(reverseMappedDefaultInferenceToConstraint.ts, 15, 5)) + + }; +}): TGuards; +>TGuards : Symbol(TGuards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 8, 11)) + +const result = setup({ +>result : Symbol(result, Decl(reverseMappedDefaultInferenceToConstraint.ts, 19, 5)) +>setup : Symbol(setup, Decl(reverseMappedDefaultInferenceToConstraint.ts, 5, 1)) + + types: { +>types : Symbol(types, Decl(reverseMappedDefaultInferenceToConstraint.ts, 19, 22)) + + context: { +>context : Symbol(context, Decl(reverseMappedDefaultInferenceToConstraint.ts, 20, 10)) + + count: 100, +>count : Symbol(count, Decl(reverseMappedDefaultInferenceToConstraint.ts, 21, 14)) + + }, + }, + guards: { +>guards : Symbol(guards, Decl(reverseMappedDefaultInferenceToConstraint.ts, 24, 4)) + + checkFoo: (_, { foo }: { foo: string }) => foo === "foo", +>checkFoo : Symbol(checkFoo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 25, 11)) +>_ : Symbol(_, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 15)) +>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 19)) +>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 28)) +>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 19)) + + alwaysTrue: (_) => true, +>alwaysTrue : Symbol(alwaysTrue, Decl(reverseMappedDefaultInferenceToConstraint.ts, 26, 61)) +>_ : Symbol(_, Decl(reverseMappedDefaultInferenceToConstraint.ts, 27, 17)) + + }, +}); + diff --git a/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types new file mode 100644 index 0000000000000..7aa986aa518d6 --- /dev/null +++ b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types @@ -0,0 +1,81 @@ +//// [tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts] //// + +=== reverseMappedDefaultInferenceToConstraint.ts === +// https://github.com/microsoft/TypeScript/issues/56241 + +interface ParameterizedObject { + type: string; +>type : string + + params?: Record; +>params : Record | undefined +} + +declare function setup< +>setup : | undefined>>(_: { types: { context: TContext;}; guards: { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }; }) => TGuards + + TContext, + TGuards extends Record, +>(_: { +>_ : { types: { context: TContext;}; guards: { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }; } + + types: { +>types : { context: TContext; } + + context: TContext; +>context : TContext + + }; + guards: { +>guards : { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; } + + [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; +>context : TContext +>params : TGuards[K] + + }; +}): TGuards; + +const result = setup({ +>result : { checkFoo: { foo: string; }; alwaysTrue: Record | undefined; } +>setup({ types: { context: { count: 100, }, }, guards: { checkFoo: (_, { foo }: { foo: string }) => foo === "foo", alwaysTrue: (_) => true, },}) : { checkFoo: { foo: string; }; alwaysTrue: Record | undefined; } +>setup : | undefined>>(_: { types: { context: TContext; }; guards: { [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; }; }) => TGuards +>{ types: { context: { count: 100, }, }, guards: { checkFoo: (_, { foo }: { foo: string }) => foo === "foo", alwaysTrue: (_) => true, },} : { types: { context: { count: number; }; }; guards: { checkFoo: (_: { count: number; }, { foo }: { foo: string; }) => boolean; alwaysTrue: (_: { count: number; }) => boolean; }; } + + types: { +>types : { context: { count: number; }; } +>{ context: { count: 100, }, } : { context: { count: number; }; } + + context: { +>context : { count: number; } +>{ count: 100, } : { count: number; } + + count: 100, +>count : number +>100 : 100 + + }, + }, + guards: { +>guards : { checkFoo: (_: { count: number; }, { foo }: { foo: string; }) => boolean; alwaysTrue: (_: { count: number; }) => boolean; } +>{ checkFoo: (_, { foo }: { foo: string }) => foo === "foo", alwaysTrue: (_) => true, } : { checkFoo: (_: { count: number; }, { foo }: { foo: string; }) => boolean; alwaysTrue: (_: { count: number; }) => boolean; } + + checkFoo: (_, { foo }: { foo: string }) => foo === "foo", +>checkFoo : (_: { count: number; }, { foo }: { foo: string; }) => boolean +>(_, { foo }: { foo: string }) => foo === "foo" : (_: { count: number; }, { foo }: { foo: string; }) => boolean +>_ : { count: number; } +>foo : string +>foo : string +>foo === "foo" : boolean +>foo : string +>"foo" : "foo" + + alwaysTrue: (_) => true, +>alwaysTrue : (_: { count: number; }) => boolean +>(_) => true : (_: { count: number; }) => boolean +>_ : { count: number; } +>true : true + + }, +}); + diff --git a/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts b/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts new file mode 100644 index 0000000000000..bb073526e7da1 --- /dev/null +++ b/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts @@ -0,0 +1,33 @@ +// @strict: true +// @noEmit: true + +// https://github.com/microsoft/TypeScript/issues/56241 + +interface ParameterizedObject { + type: string; + params?: Record; +} + +declare function setup< + TContext, + TGuards extends Record, +>(_: { + types: { + context: TContext; + }; + guards: { + [K in keyof TGuards]: (context: TContext, params: TGuards[K]) => void; + }; +}): TGuards; + +const result = setup({ + types: { + context: { + count: 100, + }, + }, + guards: { + checkFoo: (_, { foo }: { foo: string }) => foo === "foo", + alwaysTrue: (_) => true, + }, +}); From a0d8bccafdafd0bec8f8e8a61348641b83e1dcdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 5 Jan 2024 23:25:43 +0100 Subject: [PATCH 2/7] improve per-property constraints by using other available inferences --- src/compiler/checker.ts | 35 +++++++--- src/compiler/types.ts | 2 + ...MappedDefaultInferenceToConstraint.symbols | 68 +++++++++++++++++++ ...seMappedDefaultInferenceToConstraint.types | 65 ++++++++++++++++++ ...verseMappedDefaultInferenceToConstraint.ts | 28 ++++++++ 5 files changed, 190 insertions(+), 8 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 3b5060374ccde..75f7accff57b7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13689,7 +13689,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const modifiers = getMappedTypeModifiers(type.mappedType); const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; - const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType), readonlyMask && indexInfo.isReadonly)] : emptyArray; + const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.keyType, indexInfo.type, type.mappedType, type.constraintType, type.inferenceMapper), readonlyMask && indexInfo.isReadonly)] : emptyArray; const members = createSymbolTable(); const limitedConstraint = getLimitedConstraint(type); for (const prop of getPropertiesOfType(type.source)) { @@ -13724,6 +13724,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { inferredProp.links.mappedType = type.mappedType; inferredProp.links.constraintType = type.constraintType; } + if (type.inferenceMapper) { + inferredProp.links.inferenceMapper = type.inferenceMapper; + } members.set(prop.escapedName, inferredProp); } setStructuredTypeMembers(type, members, emptyArray, emptyArray, indexInfos); @@ -13993,13 +13996,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { else if ((type as ObjectType).objectFlags & ObjectFlags.ClassOrInterface) { resolveClassOrInterfaceMembers(type as InterfaceType); } - else if ((type as ReverseMappedType).objectFlags & ObjectFlags.ReverseMapped) { + else if ((type as ObjectType).objectFlags & ObjectFlags.ReverseMapped) { resolveReverseMappedTypeMembers(type as ReverseMappedType); } else if ((type as ObjectType).objectFlags & ObjectFlags.Anonymous) { resolveAnonymousTypeMembers(type as AnonymousType); } - else if ((type as MappedType).objectFlags & ObjectFlags.Mapped) { + else if ((type as ObjectType).objectFlags & ObjectFlags.Mapped) { resolveMappedTypeMembers(type as MappedType); } else { @@ -25065,10 +25068,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // For arrays and tuples we infer new arrays and tuples where the reverse mapping has been // applied to the element type(s). if (isArrayType(source)) { - return createArrayType(inferReverseMappedType(getTypeArguments(source)[0], target, constraint), isReadonlyArrayType(source)); + return createArrayType(inferReverseMappedType(numberType, getTypeArguments(source)[0], target, constraint), isReadonlyArrayType(source)); } if (isTupleType(source)) { - const elementTypes = map(getElementTypes(source), t => inferReverseMappedType(t, target, constraint)); + const elementTypes = map(getElementTypes(source), (t, i) => inferReverseMappedType(getStringLiteralType("" + i), t, target, constraint)); const elementFlags = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ? sameMap(source.target.elementFlags, f => f & ElementFlags.Optional ? ElementFlags.Required : f) : source.target.elementFlags; @@ -25086,17 +25089,26 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) { const links = getSymbolLinks(symbol); if (!links.type) { - links.type = inferReverseMappedType(symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType); + const propertyNameType = getStringLiteralType(unescapeLeadingUnderscores(symbol.escapedName)); + links.type = inferReverseMappedType(propertyNameType, symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType, symbol.links.inferenceMapper); } return links.type; } - function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type { + function inferReverseMappedType(propertyNameType: Type, sourceType: Type, target: MappedType, constraint: IndexType, inferenceMapper?: TypeMapper): Type { const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as TypeParameter; const templateType = getTemplateTypeFromMappedType(target); const inference = createInferenceInfo(typeParameter); inferTypes([inference], sourceType, templateType); - return getTypeFromInference(inference) || getBaseConstraintOfType(typeParameter) || unknownType; + const inferredType = getTypeFromInference(inference); + if (inferredType) { + return inferredType; + } + if (!inferenceMapper) { + return getBaseConstraintOfType(typeParameter) || unknownType; + } + + return instantiateType(getConstraintOfType(getIndexedAccessType(constraint.type, propertyNameType)), inferenceMapper) || unknownType; } function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator { @@ -26185,10 +26197,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const constraint = getConstraintOfTypeParameter(inference.typeParameter); if (constraint) { const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); + // TODO: decide what to do about fallback type + if (inferredType && inferredType.flags & TypeFlags.Object && (inferredType as ObjectType).objectFlags & ObjectFlags.ReverseMapped) { + (inferredType as ReverseMappedType).inferenceMapper = context.nonFixingMapper; + } if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { // If the fallback type satisfies the constraint, we pick it. Otherwise, we pick the constraint. inference.inferredType = fallbackType && context.compareTypes(fallbackType, getTypeWithThisArgument(instantiatedConstraint, fallbackType)) ? fallbackType : instantiatedConstraint; } + if (inferredType && inferredType.flags & TypeFlags.Object && (inferredType as ObjectType).objectFlags & ObjectFlags.ReverseMapped) { + (inferredType as ReverseMappedType).inferenceMapper = undefined; + } } } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e56bba5ab4859..14c896a02813e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -5945,6 +5945,7 @@ export interface ReverseMappedSymbolLinks extends TransientSymbolLinks { propertyType: Type; mappedType: MappedType; constraintType: IndexType; + inferenceMapper?: TypeMapper; } /** @internal */ @@ -6535,6 +6536,7 @@ export interface ReverseMappedType extends ObjectType { source: Type; mappedType: MappedType; constraintType: IndexType; + inferenceMapper?: TypeMapper; } /** @internal */ diff --git a/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols index aa6078e058588..3fbd5c76df940 100644 --- a/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols +++ b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.symbols @@ -84,3 +84,71 @@ const result = setup({ }, }); +declare function foo< +>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 29, 3)) + + T extends Record, +>T : Symbol(T, Decl(reverseMappedDefaultInferenceToConstraint.ts, 31, 21)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>PropertyKey : Symbol(PropertyKey, Decl(lib.es5.d.ts, --, --)) +>U : Symbol(U, Decl(reverseMappedDefaultInferenceToConstraint.ts, 32, 35)) + + U extends number | boolean, +>U : Symbol(U, Decl(reverseMappedDefaultInferenceToConstraint.ts, 32, 35)) + +>( + a: { +>a : Symbol(a, Decl(reverseMappedDefaultInferenceToConstraint.ts, 34, 2)) + + [K in keyof T]: (arg: T[K]) => void; +>K : Symbol(K, Decl(reverseMappedDefaultInferenceToConstraint.ts, 36, 5)) +>T : Symbol(T, Decl(reverseMappedDefaultInferenceToConstraint.ts, 31, 21)) +>arg : Symbol(arg, Decl(reverseMappedDefaultInferenceToConstraint.ts, 36, 21)) +>T : Symbol(T, Decl(reverseMappedDefaultInferenceToConstraint.ts, 31, 21)) +>K : Symbol(K, Decl(reverseMappedDefaultInferenceToConstraint.ts, 36, 5)) + + }, + b: U, +>b : Symbol(b, Decl(reverseMappedDefaultInferenceToConstraint.ts, 37, 4)) +>U : Symbol(U, Decl(reverseMappedDefaultInferenceToConstraint.ts, 32, 35)) + +): T; +>T : Symbol(T, Decl(reverseMappedDefaultInferenceToConstraint.ts, 31, 21)) + +declare const num: number; +>num : Symbol(num, Decl(reverseMappedDefaultInferenceToConstraint.ts, 41, 13)) + +const result1 = foo( +>result1 : Symbol(result1, Decl(reverseMappedDefaultInferenceToConstraint.ts, 43, 5)) +>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 29, 3)) + { + a: (arg) => {}, +>a : Symbol(a, Decl(reverseMappedDefaultInferenceToConstraint.ts, 44, 3)) +>arg : Symbol(arg, Decl(reverseMappedDefaultInferenceToConstraint.ts, 45, 8)) + + b: () => {}, +>b : Symbol(b, Decl(reverseMappedDefaultInferenceToConstraint.ts, 45, 19)) + + }, + num, +>num : Symbol(num, Decl(reverseMappedDefaultInferenceToConstraint.ts, 41, 13)) + +); + +const result2 = foo( +>result2 : Symbol(result2, Decl(reverseMappedDefaultInferenceToConstraint.ts, 51, 5)) +>foo : Symbol(foo, Decl(reverseMappedDefaultInferenceToConstraint.ts, 29, 3)) + { + a: (arg: 100) => {}, +>a : Symbol(a, Decl(reverseMappedDefaultInferenceToConstraint.ts, 52, 3)) +>arg : Symbol(arg, Decl(reverseMappedDefaultInferenceToConstraint.ts, 53, 8)) + + b: () => {}, +>b : Symbol(b, Decl(reverseMappedDefaultInferenceToConstraint.ts, 53, 24)) + + }, + num, +>num : Symbol(num, Decl(reverseMappedDefaultInferenceToConstraint.ts, 41, 13)) + +); + diff --git a/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types index 7aa986aa518d6..1739f49c2470d 100644 --- a/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types +++ b/tests/baselines/reference/reverseMappedDefaultInferenceToConstraint.types @@ -79,3 +79,68 @@ const result = setup({ }, }); +declare function foo< +>foo : , U extends number | boolean>(a: { [K in keyof T]: (arg: T[K]) => void; }, b: U) => T + + T extends Record, + U extends number | boolean, +>( + a: { +>a : { [K in keyof T]: (arg: T[K]) => void; } + + [K in keyof T]: (arg: T[K]) => void; +>arg : T[K] + + }, + b: U, +>b : U + +): T; + +declare const num: number; +>num : number + +const result1 = foo( +>result1 : { a: number; b: number; } +>foo( { a: (arg) => {}, b: () => {}, }, num,) : { a: number; b: number; } +>foo : , U extends number | boolean>(a: { [K in keyof T]: (arg: T[K]) => void; }, b: U) => T + { +>{ a: (arg) => {}, b: () => {}, } : { a: (arg: number) => void; b: () => void; } + + a: (arg) => {}, +>a : (arg: number) => void +>(arg) => {} : (arg: number) => void +>arg : number + + b: () => {}, +>b : () => void +>() => {} : () => void + + }, + num, +>num : number + +); + +const result2 = foo( +>result2 : { a: 100; b: number; } +>foo( { a: (arg: 100) => {}, b: () => {}, }, num,) : { a: 100; b: number; } +>foo : , U extends number | boolean>(a: { [K in keyof T]: (arg: T[K]) => void; }, b: U) => T + { +>{ a: (arg: 100) => {}, b: () => {}, } : { a: (arg: 100) => void; b: () => void; } + + a: (arg: 100) => {}, +>a : (arg: 100) => void +>(arg: 100) => {} : (arg: 100) => void +>arg : 100 + + b: () => {}, +>b : () => void +>() => {} : () => void + + }, + num, +>num : number + +); + diff --git a/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts b/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts index bb073526e7da1..ba934f6f4465e 100644 --- a/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts +++ b/tests/cases/compiler/reverseMappedDefaultInferenceToConstraint.ts @@ -31,3 +31,31 @@ const result = setup({ alwaysTrue: (_) => true, }, }); + +declare function foo< + T extends Record, + U extends number | boolean, +>( + a: { + [K in keyof T]: (arg: T[K]) => void; + }, + b: U, +): T; + +declare const num: number; + +const result1 = foo( + { + a: (arg) => {}, + b: () => {}, + }, + num, +); + +const result2 = foo( + { + a: (arg: 100) => {}, + b: () => {}, + }, + num, +); From 50f63e409cad4ac501c4d8cc8f21b4e08f8be6a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 26 Nov 2025 23:30:47 +0100 Subject: [PATCH 3/7] change strategy to reverse mapped type clones --- src/compiler/checker.ts | 49 +++++++++++++++++++++-------------------- src/compiler/types.ts | 4 ++-- 2 files changed, 27 insertions(+), 26 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 79a2190b135d7..656340d90eb42 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14640,7 +14640,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const modifiers = getMappedTypeModifiers(type.mappedType); const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; - const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.keyType, indexInfo.type, type.mappedType, type.constraintType, type.inferenceMapper) || unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray; + const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.keyType, indexInfo.type, type.mappedType, type.constraintType, type.inferenceConstraintType) || unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray; const members = createSymbolTable(); const limitedConstraint = getLimitedConstraint(type); for (const prop of getPropertiesOfType(type.source)) { @@ -14675,9 +14675,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { inferredProp.links.mappedType = type.mappedType; inferredProp.links.constraintType = type.constraintType; } - if (type.inferenceMapper) { - inferredProp.links.inferenceMapper = type.inferenceMapper; - } + inferredProp.links.inferenceConstraintType = type.inferenceConstraintType; members.set(prop.escapedName, inferredProp); } setStructuredTypeMembers(type, members, emptyArray, emptyArray, indexInfos); @@ -26425,29 +26423,21 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const links = getSymbolLinks(symbol); if (!links.type) { const propertyNameType = getStringLiteralType(unescapeLeadingUnderscores(symbol.escapedName)); - links.type = inferReverseMappedType(propertyNameType, symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType, symbol.links.inferenceMapper) || unknownType; + links.type = inferReverseMappedType(propertyNameType, symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType, symbol.links.inferenceConstraintType) || unknownType; } return links.type; } - function inferReverseMappedTypeWorker(propertyNameType: Type, sourceType: Type, target: MappedType, constraint: IndexType, inferenceMapper?: TypeMapper): Type { + function inferReverseMappedTypeWorker(propertyNameType: Type, sourceType: Type, target: MappedType, constraint: IndexType, inferenceConstraintType: Type | undefined): Type { const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as TypeParameter; const templateType = getTemplateTypeFromMappedType(target); const inference = createInferenceInfo(typeParameter); inferTypes([inference], sourceType, templateType); - const inferredType = getTypeFromInference(inference); - if (inferredType) { - return inferredType; - } - if (!inferenceMapper) { - return getBaseConstraintOfType(typeParameter) || unknownType; - } - - return instantiateType(getConstraintOfType(getIndexedAccessType(constraint.type, propertyNameType)), inferenceMapper) || unknownType; + return getTypeFromInference(inference) || inferenceConstraintType && getIndexedAccessTypeOrUndefined(inferenceConstraintType, propertyNameType) || unknownType; } - function inferReverseMappedType(propertyNameType: Type, source: Type, target: MappedType, constraint: IndexType, inferenceMapper?: TypeMapper): Type | undefined { - const cacheKey = source.id + "," + target.id + "," + constraint.id; + function inferReverseMappedType(propertyNameType: Type, source: Type, target: MappedType, constraint: IndexType, inferenceConstraintType?: Type): Type | undefined { + const cacheKey = source.id + "," + target.id + "," + constraint.id + "," + (inferenceConstraintType?.id ?? "0"); if (reverseMappedCache.has(cacheKey)) { return reverseMappedCache.get(cacheKey) || unknownType; } @@ -26458,7 +26448,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (isDeeplyNestedType(target, reverseMappedTargetStack, reverseMappedTargetStack.length, 2)) reverseExpandingFlags |= ExpandingFlags.Target; let type; if (reverseExpandingFlags !== ExpandingFlags.Both) { - type = inferReverseMappedTypeWorker(propertyNameType, source, target, constraint, inferenceMapper); + type = inferReverseMappedTypeWorker(propertyNameType, source, target, constraint, inferenceConstraintType); } reverseMappedSourceStack.pop(); reverseMappedTargetStack.pop(); @@ -27583,22 +27573,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const constraint = getConstraintOfTypeParameter(inference.typeParameter); if (constraint) { const instantiatedConstraint = instantiateType(constraint, context.nonFixingMapper); - // TODO: decide what to do about fallback type - if (inferredType && inferredType.flags & TypeFlags.Object && (inferredType as ObjectType).objectFlags & ObjectFlags.ReverseMapped) { - (inferredType as ReverseMappedType).inferenceMapper = context.nonFixingMapper; + inferredType = cloneWithInferenceConstraintIfNeeded(inferredType, instantiatedConstraint); + if (inferredType) { + inference.inferredType = inferredType; } if (!inferredType || !context.compareTypes(inferredType, getTypeWithThisArgument(instantiatedConstraint, inferredType))) { // If the fallback type satisfies the constraint, we pick it. Otherwise, we pick the constraint. + fallbackType = cloneWithInferenceConstraintIfNeeded(fallbackType, instantiatedConstraint); inference.inferredType = fallbackType && context.compareTypes(fallbackType, getTypeWithThisArgument(instantiatedConstraint, fallbackType)) ? fallbackType : instantiatedConstraint; } - if (inferredType && inferredType.flags & TypeFlags.Object && (inferredType as ObjectType).objectFlags & ObjectFlags.ReverseMapped) { - (inferredType as ReverseMappedType).inferenceMapper = undefined; - } } clearActiveMapperCaches(); } return inference.inferredType; + + function cloneWithInferenceConstraintIfNeeded(type: Type | undefined, inferenceConstraintType: Type) { + if (!type || !(type.flags & TypeFlags.Object) || !((type as ObjectType).objectFlags & ObjectFlags.ReverseMapped)) { + return type; + } + const reversed = type as ReverseMappedType; + const clone = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType; + clone.source = reversed.source; + clone.mappedType = reversed.mappedType; + clone.constraintType = reversed.constraintType; + clone.inferenceConstraintType = inferenceConstraintType; + return clone; + } } function getDefaultTypeArgumentType(isInJavaScriptFile: boolean): Type { diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b2e4c5c355f9f..b6999517420bd 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -6155,7 +6155,7 @@ export interface ReverseMappedSymbolLinks extends TransientSymbolLinks { propertyType: Type; mappedType: MappedType; constraintType: IndexType; - inferenceMapper?: TypeMapper; + inferenceConstraintType?: Type; } /** @internal */ @@ -6788,7 +6788,7 @@ export interface ReverseMappedType extends ObjectType { source: Type; mappedType: MappedType; constraintType: IndexType; - inferenceMapper?: TypeMapper; + inferenceConstraintType?: Type; } /** @internal */ From 75408c715f8ffaab9ccb809f9762563eab5f9498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Wed, 26 Nov 2025 23:42:27 +0100 Subject: [PATCH 4/7] fixed `propertyNameType` passed down in case of tuples --- src/compiler/checker.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 656340d90eb42..a75cebca32afc 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -26401,7 +26401,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return createArrayType(elementType, isReadonlyArrayType(source)); } if (isTupleType(source)) { - const elementTypes = map(getElementTypes(source), (t, i) => inferReverseMappedType(getStringLiteralType("" + i), t, target, constraint)); + const fixedLength = source.target.fixedLength; + const elementTypes = map(getElementTypes(source), (t, i) => inferReverseMappedType(i < fixedLength ? getStringLiteralType("" + i) : numberType, t, target, constraint)); if (!every(elementTypes, (t): t is Type => !!t)) { return undefined; } From 93c2643207d8174593fcfea20e11c2d74238c54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 29 Nov 2025 08:10:01 +0100 Subject: [PATCH 5/7] simplify and cache --- src/compiler/checker.ts | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a75cebca32afc..881b0c96f3124 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14640,7 +14640,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { const modifiers = getMappedTypeModifiers(type.mappedType); const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; - const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.keyType, indexInfo.type, type.mappedType, type.constraintType, type.inferenceConstraintType) || unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray; + const indexInfos = indexInfo ? [createIndexInfo(stringType, inferReverseMappedType(indexInfo.type, type.mappedType, type.constraintType, type.inferenceConstraintType && getIndexedAccessTypeOrUndefined(type.inferenceConstraintType, indexInfo.keyType)) ?? unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray; const members = createSymbolTable(); const limitedConstraint = getLimitedConstraint(type); for (const prop of getPropertiesOfType(type.source)) { @@ -26394,15 +26394,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // For arrays and tuples we infer new arrays and tuples where the reverse mapping has been // applied to the element type(s). if (isArrayType(source)) { - const elementType = inferReverseMappedType(numberType, getTypeArguments(source)[0], target, constraint); + const elementType = inferReverseMappedType(getTypeArguments(source)[0], target, constraint); if (!elementType) { return undefined; } return createArrayType(elementType, isReadonlyArrayType(source)); } if (isTupleType(source)) { - const fixedLength = source.target.fixedLength; - const elementTypes = map(getElementTypes(source), (t, i) => inferReverseMappedType(i < fixedLength ? getStringLiteralType("" + i) : numberType, t, target, constraint)); + const elementTypes = map(getElementTypes(source), t => inferReverseMappedType(t, target, constraint)); if (!every(elementTypes, (t): t is Type => !!t)) { return undefined; } @@ -26423,22 +26422,22 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol): Type { const links = getSymbolLinks(symbol); if (!links.type) { - const propertyNameType = getStringLiteralType(unescapeLeadingUnderscores(symbol.escapedName)); - links.type = inferReverseMappedType(propertyNameType, symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType, symbol.links.inferenceConstraintType) || unknownType; + const reverseConstraint = symbol.links.inferenceConstraintType && getIndexedAccessTypeOrUndefined(symbol.links.inferenceConstraintType, getStringLiteralType(unescapeLeadingUnderscores(symbol.escapedName))); + links.type = inferReverseMappedType(symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType, reverseConstraint) ?? unknownType; } return links.type; } - function inferReverseMappedTypeWorker(propertyNameType: Type, sourceType: Type, target: MappedType, constraint: IndexType, inferenceConstraintType: Type | undefined): Type { + function inferReverseMappedTypeWorker(sourceType: Type, target: MappedType, constraint: IndexType, reverseMappedConstraint: Type | undefined): Type { const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as TypeParameter; const templateType = getTemplateTypeFromMappedType(target); const inference = createInferenceInfo(typeParameter); inferTypes([inference], sourceType, templateType); - return getTypeFromInference(inference) || inferenceConstraintType && getIndexedAccessTypeOrUndefined(inferenceConstraintType, propertyNameType) || unknownType; + return getTypeFromInference(inference) ?? reverseMappedConstraint ?? unknownType; } - function inferReverseMappedType(propertyNameType: Type, source: Type, target: MappedType, constraint: IndexType, inferenceConstraintType?: Type): Type | undefined { - const cacheKey = source.id + "," + target.id + "," + constraint.id + "," + (inferenceConstraintType?.id ?? "0"); + function inferReverseMappedType(source: Type, target: MappedType, constraint: IndexType, reverseMappedConstraint?: Type): Type | undefined { + const cacheKey = source.id + "," + target.id + "," + constraint.id + "," + (reverseMappedConstraint?.id ?? "0"); if (reverseMappedCache.has(cacheKey)) { return reverseMappedCache.get(cacheKey) || unknownType; } @@ -26449,7 +26448,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (isDeeplyNestedType(target, reverseMappedTargetStack, reverseMappedTargetStack.length, 2)) reverseExpandingFlags |= ExpandingFlags.Target; let type; if (reverseExpandingFlags !== ExpandingFlags.Both) { - type = inferReverseMappedTypeWorker(propertyNameType, source, target, constraint, inferenceConstraintType); + type = inferReverseMappedTypeWorker(source, target, constraint, reverseMappedConstraint); } reverseMappedSourceStack.pop(); reverseMappedTargetStack.pop(); @@ -27594,11 +27593,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return type; } const reversed = type as ReverseMappedType; + const cacheKey = reversed.source.id + "," + reversed.mappedType.id + "," + reversed.constraintType.id + "," + inferenceConstraintType.id; + + if (reverseHomomorphicMappedCache.has(cacheKey)) { + return reverseHomomorphicMappedCache.get(cacheKey); + } + const clone = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType; clone.source = reversed.source; clone.mappedType = reversed.mappedType; clone.constraintType = reversed.constraintType; clone.inferenceConstraintType = inferenceConstraintType; + + reverseHomomorphicMappedCache.set(cacheKey, clone); return clone; } } From 471ad1f17c3e27131792b9e7e3655b41953b93b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Sat, 29 Nov 2025 09:07:54 +0100 Subject: [PATCH 6/7] fmt --- src/compiler/checker.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 881b0c96f3124..1c157e4c947e6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -27598,13 +27598,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (reverseHomomorphicMappedCache.has(cacheKey)) { return reverseHomomorphicMappedCache.get(cacheKey); } - + const clone = createObjectType(ObjectFlags.ReverseMapped | ObjectFlags.Anonymous, /*symbol*/ undefined) as ReverseMappedType; clone.source = reversed.source; clone.mappedType = reversed.mappedType; clone.constraintType = reversed.constraintType; clone.inferenceConstraintType = inferenceConstraintType; - + reverseHomomorphicMappedCache.set(cacheKey, clone); return clone; } From a4705efedfc96c179acd1d3f8594b28d98a6a826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Fri, 5 Dec 2025 12:07:45 +0100 Subject: [PATCH 7/7] add silly test --- ...seMappedFromContravariantPosition1.symbols | 44 +++++++++++++++ ...erseMappedFromContravariantPosition1.types | 53 +++++++++++++++++++ ...reverseMappedFromContravariantPosition1.ts | 17 ++++++ 3 files changed, 114 insertions(+) create mode 100644 tests/baselines/reference/reverseMappedFromContravariantPosition1.symbols create mode 100644 tests/baselines/reference/reverseMappedFromContravariantPosition1.types create mode 100644 tests/cases/compiler/reverseMappedFromContravariantPosition1.ts diff --git a/tests/baselines/reference/reverseMappedFromContravariantPosition1.symbols b/tests/baselines/reference/reverseMappedFromContravariantPosition1.symbols new file mode 100644 index 0000000000000..7ee320b6666ec --- /dev/null +++ b/tests/baselines/reference/reverseMappedFromContravariantPosition1.symbols @@ -0,0 +1,44 @@ +//// [tests/cases/compiler/reverseMappedFromContravariantPosition1.ts] //// + +=== reverseMappedFromContravariantPosition1.ts === +declare function test1( +>test1 : Symbol(test1, Decl(reverseMappedFromContravariantPosition1.ts, 0, 0)) +>T : Symbol(T, Decl(reverseMappedFromContravariantPosition1.ts, 0, 23)) + + cb: (arg: { [K in keyof T]: { prop: T[K] } }) => void, +>cb : Symbol(cb, Decl(reverseMappedFromContravariantPosition1.ts, 0, 26)) +>arg : Symbol(arg, Decl(reverseMappedFromContravariantPosition1.ts, 1, 7)) +>K : Symbol(K, Decl(reverseMappedFromContravariantPosition1.ts, 1, 15)) +>T : Symbol(T, Decl(reverseMappedFromContravariantPosition1.ts, 0, 23)) +>prop : Symbol(prop, Decl(reverseMappedFromContravariantPosition1.ts, 1, 31)) +>T : Symbol(T, Decl(reverseMappedFromContravariantPosition1.ts, 0, 23)) +>K : Symbol(K, Decl(reverseMappedFromContravariantPosition1.ts, 1, 15)) + +): T; +>T : Symbol(T, Decl(reverseMappedFromContravariantPosition1.ts, 0, 23)) + +type Arg = { +>Arg : Symbol(Arg, Decl(reverseMappedFromContravariantPosition1.ts, 2, 5)) + + a: { +>a : Symbol(a, Decl(reverseMappedFromContravariantPosition1.ts, 4, 12)) + + prop: number; +>prop : Symbol(prop, Decl(reverseMappedFromContravariantPosition1.ts, 5, 6)) + + }; + b: { +>b : Symbol(b, Decl(reverseMappedFromContravariantPosition1.ts, 7, 4)) + + prop: boolean; +>prop : Symbol(prop, Decl(reverseMappedFromContravariantPosition1.ts, 8, 6)) + + }; +}; + +const result1 = test1((arg: Arg) => {}); +>result1 : Symbol(result1, Decl(reverseMappedFromContravariantPosition1.ts, 13, 5)) +>test1 : Symbol(test1, Decl(reverseMappedFromContravariantPosition1.ts, 0, 0)) +>arg : Symbol(arg, Decl(reverseMappedFromContravariantPosition1.ts, 13, 23)) +>Arg : Symbol(Arg, Decl(reverseMappedFromContravariantPosition1.ts, 2, 5)) + diff --git a/tests/baselines/reference/reverseMappedFromContravariantPosition1.types b/tests/baselines/reference/reverseMappedFromContravariantPosition1.types new file mode 100644 index 0000000000000..a3386a3c6585e --- /dev/null +++ b/tests/baselines/reference/reverseMappedFromContravariantPosition1.types @@ -0,0 +1,53 @@ +//// [tests/cases/compiler/reverseMappedFromContravariantPosition1.ts] //// + +=== reverseMappedFromContravariantPosition1.ts === +declare function test1( +>test1 : (cb: (arg: { [K in keyof T]: { prop: T[K]; }; }) => void) => T +> : ^ ^^ ^^ ^^^^^ + + cb: (arg: { [K in keyof T]: { prop: T[K] } }) => void, +>cb : (arg: { [K in keyof T]: { prop: T[K]; }; }) => void +> : ^ ^^ ^^^^^ +>arg : { [K in keyof T]: { prop: T[K]; }; } +> : ^^^ ^^^^^^^^^^^^^^^^^^^^^^ ^^^^^^ +>prop : T[K] +> : ^^^^ + +): T; + +type Arg = { +>Arg : Arg +> : ^^^ + + a: { +>a : { prop: number; } +> : ^^^^^^^^ ^^^ + + prop: number; +>prop : number +> : ^^^^^^ + + }; + b: { +>b : { prop: boolean; } +> : ^^^^^^^^ ^^^ + + prop: boolean; +>prop : boolean +> : ^^^^^^^ + + }; +}; + +const result1 = test1((arg: Arg) => {}); +>result1 : { a: number; b: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>test1((arg: Arg) => {}) : { a: number; b: boolean; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ +>test1 : (cb: (arg: { [K in keyof T]: { prop: T[K]; }; }) => void) => T +> : ^ ^^ ^^ ^^^^^ +>(arg: Arg) => {} : (arg: Arg) => void +> : ^ ^^ ^^^^^^^^^ +>arg : Arg +> : ^^^ + diff --git a/tests/cases/compiler/reverseMappedFromContravariantPosition1.ts b/tests/cases/compiler/reverseMappedFromContravariantPosition1.ts new file mode 100644 index 0000000000000..9fcd689f7799e --- /dev/null +++ b/tests/cases/compiler/reverseMappedFromContravariantPosition1.ts @@ -0,0 +1,17 @@ +// @strict: true +// @noEmit: true + +declare function test1( + cb: (arg: { [K in keyof T]: { prop: T[K] } }) => void, +): T; + +type Arg = { + a: { + prop: number; + }; + b: { + prop: boolean; + }; +}; + +const result1 = test1((arg: Arg) => {});