Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e90edb3
Improve inference for context sensitive functions within reverse mapp…
Andarist Apr 25, 2023
557cd99
use more specialized `Debug.assertNode`
Andarist Apr 26, 2023
4774f8f
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist May 24, 2023
b7eae05
check freshness of the source type before attempting to infer from in…
Andarist May 24, 2023
a0804b5
get value declaration from the passed in argument
Andarist May 25, 2023
57b54c9
Ignore `valueDeclaration` in `getDeclarationModifierFlagsFromSymbol` …
Andarist May 25, 2023
fae5ae6
Add an extra test case for array literals used as values of object re…
Andarist May 25, 2023
34d675a
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist May 25, 2023
a8e8a24
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist Jun 12, 2023
d71dcd2
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist Nov 27, 2025
5b33846
refactor
Andarist Nov 29, 2025
cca600f
add comments
Andarist Nov 29, 2025
e7e9125
fmt
Andarist Nov 29, 2025
5086dfa
fix cache
Andarist Nov 29, 2025
b5c6ef4
add extra tests
Andarist Nov 29, 2025
9e748b7
prefer inferring from non-context sensitive type
Andarist Dec 4, 2025
f38ceb3
use a different approach to link partial reverse mapped type with the…
Andarist Dec 5, 2025
2724523
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist Dec 5, 2025
ec14355
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist Dec 9, 2025
83b0614
Merge remote-tracking branch 'origin/main' into intra-inference-in-re…
Andarist Jan 11, 2026
7291707
update baselines
Andarist Jan 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 34 additions & 28 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ import {
InternalSymbolName,
IntersectionType,
IntersectionTypeNode,
IntraExpressionInferenceSite,
intrinsicTagNameToString,
IntrinsicType,
introducesArgumentsExoticObject,
Expand Down Expand Up @@ -13121,12 +13122,13 @@ 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.type, type.mappedType, type.constraintType, /*sourceValueDeclaration*/ undefined), readonlyMask && indexInfo.isReadonly)] : emptyArray;
const members = createSymbolTable();
for (const prop of getPropertiesOfType(type.source)) {
const checkFlags = CheckFlags.ReverseMapped | (readonlyMask && isReadonlySymbol(prop) ? CheckFlags.Readonly : 0);
const inferredProp = createSymbol(SymbolFlags.Property | prop.flags & optionalMask, prop.escapedName, checkFlags) as ReverseMappedSymbol;
inferredProp.declarations = prop.declarations;
inferredProp.valueDeclaration = prop.valueDeclaration;
inferredProp.links.nameType = getSymbolLinks(prop).nameType;
inferredProp.links.propertyType = getTypeOfSymbol(prop);
if (type.constraintType.type.flags & TypeFlags.IndexedAccess
Expand Down Expand Up @@ -23916,7 +23918,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (!inference.isFixed) {
// Before we commit to a particular inference (and thus lock out any further inferences),
// we infer from any intra-expression inference sites we have collected.
inferFromIntraExpressionSites(context);
if (context.intraExpressionInferenceSites) {
inferFromIntraExpressionSites(context.inferences, context.intraExpressionInferenceSites);
}
clearCachedInferences(context.inferences);
inference.isFixed = true;
}
Expand Down Expand Up @@ -23955,17 +23959,14 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// arrow function. This happens automatically when the arrow functions are discrete arguments (because we
// infer from each argument before processing the next), but when the arrow functions are elements of an
// object or array literal, we need to perform intra-expression inferences early.
function inferFromIntraExpressionSites(context: InferenceContext) {
if (context.intraExpressionInferenceSites) {
for (const { node, type } of context.intraExpressionInferenceSites) {
const contextualType = node.kind === SyntaxKind.MethodDeclaration ?
getContextualTypeForObjectLiteralMethod(node as MethodDeclaration, ContextFlags.NoConstraints) :
getContextualType(node, ContextFlags.NoConstraints);
if (contextualType) {
inferTypes(context.inferences, type, contextualType);
}
function inferFromIntraExpressionSites(inferences: InferenceInfo[], intraExpressionInferenceSites: IntraExpressionInferenceSite[]) {
for (const { node, type } of intraExpressionInferenceSites) {
const contextualType = node.kind === SyntaxKind.MethodDeclaration ?
getContextualTypeForObjectLiteralMethod(node as MethodDeclaration, ContextFlags.NoConstraints) :
getContextualType(node, ContextFlags.NoConstraints);
if (contextualType) {
inferTypes(inferences, type, contextualType);
}
context.intraExpressionInferenceSites = undefined;
}
}

Expand Down Expand Up @@ -24089,29 +24090,19 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return type;
}

// We consider a type to be partially inferable if it isn't marked non-inferable or if it is
// an object literal type with at least one property of an inferable type. For example, an object
// literal { a: 123, b: x => true } is marked non-inferable because it contains a context sensitive
// arrow function, but is considered partially inferable because property 'a' has an inferable type.
function isPartiallyInferableType(type: Type): boolean {
return !(getObjectFlags(type) & ObjectFlags.NonInferrableType) ||
isObjectLiteralType(type) && some(getPropertiesOfType(type), prop => isPartiallyInferableType(getTypeOfSymbol(prop))) ||
isTupleType(type) && some(getElementTypes(type), isPartiallyInferableType);
}

function createReverseMappedType(source: Type, target: MappedType, constraint: IndexType) {
// We consider a source type reverse mappable if it has a string index signature or if
// it has one or more properties and is of a partially inferable type.
if (!(getIndexInfoOfType(source, stringType) || getPropertiesOfType(source).length !== 0 && isPartiallyInferableType(source))) {
// it has one or more properties
if (!getIndexInfoOfType(source, stringType) && !getPropertiesOfType(source).length) {
return undefined;
}
// 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(getTypeArguments(source)[0], target, constraint, /*sourceValueDeclaration*/ undefined), isReadonlyArrayType(source));
}
if (isTupleType(source)) {
const elementTypes = map(getElementTypes(source), t => inferReverseMappedType(t, target, constraint));
const elementTypes = map(getElementTypes(source), t => inferReverseMappedType(t, target, constraint, /*sourceValueDeclaration*/ undefined));
const elementFlags = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ?
sameMap(source.target.elementFlags, f => f & ElementFlags.Optional ? ElementFlags.Required : f) :
source.target.elementFlags;
Expand All @@ -24129,16 +24120,31 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) {
Comment thread
Andarist marked this conversation as resolved.
Outdated
const links = getSymbolLinks(symbol);
if (!links.type) {
links.type = inferReverseMappedType(symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType);
links.type = inferReverseMappedType(symbol.links.propertyType, symbol.links.mappedType, symbol.links.constraintType, symbol.valueDeclaration);
}
return links.type;
}

function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type {
function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType, sourceValueDeclaration: Declaration | undefined): Type {
const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as TypeParameter;
const templateType = getTemplateTypeFromMappedType(target);
const inference = createInferenceInfo(typeParameter);
inferTypes([inference], sourceType, templateType);
if (sourceValueDeclaration && getObjectFlags(sourceType) & (ObjectFlags.FreshLiteral | ObjectFlags.ArrayLiteral)) {
const initializerDeclaration = sourceValueDeclaration.kind === SyntaxKind.PropertyAssignment ?
(sourceValueDeclaration as PropertyAssignment).initializer :
sourceValueDeclaration;
const intraExpressionInferenceSites = getInferenceContext(initializerDeclaration)?.intraExpressionInferenceSites?.filter(site => isNodeDescendantOf(site.node, initializerDeclaration));
if (intraExpressionInferenceSites?.length) {
const templateType = (getApparentTypeOfContextualType(initializerDeclaration.parent.parent as Expression, ContextFlags.NoConstraints) as MappedType).templateType;
if (templateType) {
Debug.assertNode(initializerDeclaration, isExpressionNode);
pushContextualType(initializerDeclaration as any as Expression, templateType, /*isCache*/ false);
inferFromIntraExpressionSites([inference], intraExpressionInferenceSites);
popContextualType();
}
}
}
return getTypeFromInference(inference) || unknownType;
}

Expand Down
5 changes: 3 additions & 2 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7539,13 +7539,14 @@ export function getCheckFlags(symbol: Symbol): CheckFlags {

/** @internal */
export function getDeclarationModifierFlagsFromSymbol(s: Symbol, isWrite = false): ModifierFlags {
if (s.valueDeclaration) {
const checkFlags = getCheckFlags(s);
if (!(checkFlags & CheckFlags.ReverseMapped) && s.valueDeclaration) {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since I added .valueDeclaration to reverse mapped properties, we need to ignore this here - otherwise, we break stripping of readonly modifiers that is required by #12589

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, this change just maintains compatibility with the old behavior as 0 was always returned from here in the implicit case of checkFlags & CheckFlags.ReverseMapped

const declaration = (isWrite && s.declarations && find(s.declarations, isSetAccessorDeclaration))
|| (s.flags & SymbolFlags.GetAccessor && find(s.declarations, isGetAccessorDeclaration)) || s.valueDeclaration;
const flags = getCombinedModifierFlags(declaration);
return s.parent && s.parent.flags & SymbolFlags.Class ? flags : flags & ~ModifierFlags.AccessibilityModifier;
}
if (getCheckFlags(s) & CheckFlags.Synthetic) {
if (checkFlags & CheckFlags.Synthetic) {
// NOTE: potentially unchecked cast to TransientSymbol
const checkFlags = (s as TransientSymbol).links.checkFlags;
const accessModifier = checkFlags & CheckFlags.ContainsPrivate ? ModifierFlags.Private :
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(67,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(71,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(80,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(86,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(95,21): error TS18046: 'x' is of type 'unknown'.
tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts(101,21): error TS18046: 'x' is of type 'unknown'.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All of those relate to the tuple-based tests that I added and those are not expected.

It doesn't work because in tuples both T and T[K] are not reverse mapped symbols/types and thus the inferReverseMappedType isn't called from within checkExpressionWithContextualType (and only within that call intraExpressionInferenceSites and "pushed inference contexts" are available).

With objects createReverseMappedType only creates a ReverseMapped type and doesn't do much else. inferReverseMappedType gets called for properties later from getTypeOfReverseMappedSymbol). However, with tuples it calls inferReverseMappedType eagerly:
https://github.dev/microsoft/TypeScript/blob/eb014a26522dd809ae4d0e85634a62eabda2755a/src/compiler/checker.ts#L24090-L24101

I could try to fix this in this PR but I also feel like this would require changes that could be moved to a separate followup PR. This PR could focus on the base mechanism for this improvement and on object-based cases.



==== tests/cases/conformance/types/typeRelationships/typeInference/intraExpressionInferencesReverseMappedTypes.ts (6 errors) ====
// repro cases based on https://github.com/microsoft/TypeScript/issues/53018

declare function f<T>(
arg: {
[K in keyof T]: {
produce: (n: string) => T[K];
consume: (x: T[K]) => void;
};
}
): T;

const res1 = f({
a: {
produce: (n) => n,
consume: (x) => x.toLowerCase(),
},
b: {
produce: (n) => ({ v: n }),
consume: (x) => x.v.toLowerCase(),
},
});

const res2 = f({
a: {
produce: function () {
return "hello";
},
consume: (x) => x.toLowerCase(),
},
b: {
produce: function () {
return { v: "hello" };
},
consume: (x) => x.v.toLowerCase(),
},
});

const res3 = f({
a: {
produce() {
return "hello";
},
consume: (x) => x.toLowerCase(),
},
b: {
produce() {
return { v: "hello" };
},
consume: (x) => x.v.toLowerCase(),
},
});

declare function f2<T extends unknown[]>(
arg: [
...{
[K in keyof T]: {
produce: (n: string) => T[K];
consume: (x: T[K]) => void;
};
}
]
): T;

const res4 = f2([
{
produce: (n) => n,
consume: (x) => x.toLowerCase(),
~
!!! error TS18046: 'x' is of type 'unknown'.
},
{
produce: (n) => ({ v: n }),
consume: (x) => x.v.toLowerCase(),
~
!!! error TS18046: 'x' is of type 'unknown'.
},
]);

const res5 = f2([
{
produce: function () {
return "hello";
},
consume: (x) => x.toLowerCase(),
~
!!! error TS18046: 'x' is of type 'unknown'.
},
{
produce: function () {
return { v: "hello" };
},
consume: (x) => x.v.toLowerCase(),
~
!!! error TS18046: 'x' is of type 'unknown'.
},
]);

const res6 = f2([
{
produce() {
return "hello";
},
consume: (x) => x.toLowerCase(),
~
!!! error TS18046: 'x' is of type 'unknown'.
},
{
produce() {
return { v: "hello" };
},
consume: (x) => x.v.toLowerCase(),
~
!!! error TS18046: 'x' is of type 'unknown'.
},
]);

declare function f3<T>(
arg: {
[K in keyof T]: {
other: number,
produce: (n: string) => T[K];
consume: (x: T[K]) => void;
};
}
): T;

const res7 = f3({
a: {
other: 42,
produce: (n) => n,
consume: (x) => x.toLowerCase(),
},
b: {
other: 100,
produce: (n) => ({ v: n }),
consume: (x) => x.v.toLowerCase(),
},
});

declare function f4<T>(
arg: {
[K in keyof T]: [
(n: string) => T[K],
(x: T[K]) => void
];
}
): T;

const res8 = f4({
a: [
(n) => n,
(x) => x.toLowerCase(),
],
b: [
(n) => ({ v: n }),
(x) => x.v.toLowerCase(),
],
});

Loading