Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Infer intersected reverse mapped types #52062

Closed
31 changes: 22 additions & 9 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16100,7 +16100,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const type = elementTypes[i];
const flags = target.elementFlags[i];
if (flags & ElementFlags.Variadic) {
if (type.flags & TypeFlags.InstantiableNonPrimitive || isGenericMappedType(type)) {
if (type.flags & TypeFlags.InstantiableNonPrimitive || everyContainedType(type, isGenericMappedType)) {
// Generic variadic elements stay as they are.
addElement(type, ElementFlags.Variadic, target.labeledElementDeclarations?.[i]);
}
Expand Down Expand Up @@ -29027,13 +29027,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {

function getTypeOfPropertyOfContextualType(type: Type, name: __String, nameType?: Type) {
return mapType(type, t => {
if (isGenericMappedType(t) && !t.declaration.nameType) {
const constraint = getConstraintTypeFromMappedType(t);
const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint;
const propertyNameType = nameType || getStringLiteralType(unescapeLeadingUnderscores(name));
if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) {
return substituteIndexedMappedType(t, propertyNameType);
}
if (everyContainedType(t, t => isGenericMappedType(t) && !t.declaration.nameType)) {
Copy link
Member

Choose a reason for hiding this comment

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

Actually, thinking about it, shouldn't we just do this for each isGenericMappedType(t) && !t.declaration.nameType within an intersection, and not only do this substitution if every member is such? (And still do the other StructuredType behaviors for other intersection members). This way a Results<T> & Errors<E> & {x?: string} still works.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right, so I actually... have another PR that does exactly that 🤣 This one here is intentionally conservative when it comes to the scope of the change, especially since some other changes (outside of getTypeOfPropertyOfContextualType) had to be made to support this use case. So it's not like that other PR completely replaces this one.

The PR in question is this: #52095 . Actually, one version of it was already merged into 4.8-beta but it was reverted cause @reduxjs/toolkit regressed with it. However, it only regressed because of how I've handled intersections of types with concrete properties with types with index signatures.

In the original version of this PR (#48668) I decided to prefer concrete properties over index signatures, so (IIRC) if one of the intersected members had a concrete property for the requested one then index signatures on other intersected members were skipped over. This wasn't strictly needed to fix the issue that I was fixing but it was something that made sense to me at the time.

Either way... if you could take a look at that other PR (#52095), I would highly appreciate it. There is some funky stuff going on there with index signatures and that part is not correct - the problem is that I'm not sure how to adjust it since I'm not sure what exact effect are we aiming for there. Mixing index signatures with concrete properties through intersections is weird (like { foo: number } & { [key: string]: boolean }). This particular place in the code doesn't exactly deal with concrete types either... so perhaps a guiding principle should be that properties should be pulled from types that can still resolve to concrete properties? I'm not sure how checking that would look like though - so perhaps we should just ignore it and always pull from index signatures?

Copy link
Member

Choose a reason for hiding this comment

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

Mmmmm, I'd rather not take this one with a known drawback like that - maybe it's best to just merge the two PRs, then?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

To keep the spirit of minimal changes I would probably prefer to keep them separate (if you prefer to merge them, then I won't oppose it though, at the end of the day I'd like both improvements to find their way into the language :P). I don't mind this one staying on the sidelines and waiting for the other one to land though. I would love for the other one to get some traction in terms of being reviewed 😜

Copy link
Member

Choose a reason for hiding this comment

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

I think that the common improvements need to be on both PRs (and the conflicts can be resolved later), or both PRs should be merged, yeah.

const newTypes = mapDefined(t.flags & TypeFlags.Intersection ? (t as IntersectionType).types : [t], t => {
const mappedType = t as MappedType;
const constraint = getConstraintTypeFromMappedType(mappedType);
const constraintOfConstraint = getBaseConstraintOfType(constraint) || constraint;
const propertyNameType = nameType || getStringLiteralType(unescapeLeadingUnderscores(name));
if (isTypeAssignableTo(propertyNameType, constraintOfConstraint)) {
return substituteIndexedMappedType(mappedType, propertyNameType);
}
});
return newTypes.length ? getIntersectionType(newTypes) : undefined;
}
else if (t.flags & TypeFlags.StructuredType) {
const prop = getPropertyOfType(t, name);
Expand Down Expand Up @@ -29248,6 +29252,10 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
);
}

function getApparentTypeOfInstantiatedContextualType(type: Type) {
return getObjectFlags(type) & ObjectFlags.Mapped ? type : getApparentType(type);
}

// Return the contextual type for a given expression node. During overload resolution, a contextual type may temporarily
// be "pushed" onto a node using the contextualType property.
function getApparentTypeOfContextualType(node: Expression | MethodDeclaration, contextFlags: ContextFlags | undefined): Type | undefined {
Expand All @@ -29262,7 +29270,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// That would evaluate mapped types with array or tuple type constraints too eagerly
// and thus it would prevent `getTypeOfPropertyOfContextualType` from obtaining per-position contextual type for elements of array literal expressions.
// Apparent type of other mapped types is already the mapped type itself so we can just avoid calling `getApparentType` here for all mapped types.
t => getObjectFlags(t) & ObjectFlags.Mapped ? t : getApparentType(t),
t => {
if (t.flags & TypeFlags.Intersection) {
return getIntersectionType(map((t as IntersectionType).types, getApparentTypeOfInstantiatedContextualType));
}
return getApparentTypeOfInstantiatedContextualType(t);
},
/*noReductions*/ true
);
return apparentType.flags & TypeFlags.Union && isObjectLiteralExpression(node) ? discriminateContextualTypeByObjectMembers(node, apparentType as UnionType) :
Expand Down
264 changes: 264 additions & 0 deletions tests/baselines/reference/reverseMappedIntersectionInference.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,264 @@
=== tests/cases/compiler/reverseMappedIntersectionInference.ts ===
type Results<T> = {
>Results : Symbol(Results, Decl(reverseMappedIntersectionInference.ts, 0, 0))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 0, 13))

[K in keyof T]: {
>K : Symbol(K, Decl(reverseMappedIntersectionInference.ts, 1, 3))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 0, 13))

data: T[K];
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 1, 19))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 0, 13))
>K : Symbol(K, Decl(reverseMappedIntersectionInference.ts, 1, 3))

onSuccess: (data: T[K]) => void;
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 2, 15))
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 3, 16))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 0, 13))
>K : Symbol(K, Decl(reverseMappedIntersectionInference.ts, 1, 3))

};
};

type Errors<E> = {
>Errors : Symbol(Errors, Decl(reverseMappedIntersectionInference.ts, 5, 2))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 7, 12))

[K in keyof E]: {
>K : Symbol(K, Decl(reverseMappedIntersectionInference.ts, 8, 3))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 7, 12))

error: E[K];
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 8, 19))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 7, 12))
>K : Symbol(K, Decl(reverseMappedIntersectionInference.ts, 8, 3))

onError: (data: E[K]) => void;
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 9, 16))
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 10, 14))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 7, 12))
>K : Symbol(K, Decl(reverseMappedIntersectionInference.ts, 8, 3))

};
};

declare function withKeyedObj<T, E>(
>withKeyedObj : Symbol(withKeyedObj, Decl(reverseMappedIntersectionInference.ts, 12, 2))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 14, 30))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 14, 32))

arg: Results<T> & Errors<E>
>arg : Symbol(arg, Decl(reverseMappedIntersectionInference.ts, 14, 36))
>Results : Symbol(Results, Decl(reverseMappedIntersectionInference.ts, 0, 0))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 14, 30))
>Errors : Symbol(Errors, Decl(reverseMappedIntersectionInference.ts, 5, 2))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 14, 32))

): [T, E];
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 14, 30))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 14, 32))

const res = withKeyedObj({
>res : Symbol(res, Decl(reverseMappedIntersectionInference.ts, 18, 5))
>withKeyedObj : Symbol(withKeyedObj, Decl(reverseMappedIntersectionInference.ts, 12, 2))

a: {
>a : Symbol(a, Decl(reverseMappedIntersectionInference.ts, 18, 26))

data: "foo",
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 19, 6))

onSuccess: (dataArg) => {
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 20, 16))
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 21, 16))

dataArg;
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 21, 16))

},
error: 404,
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 23, 6))

onError: (errorArg) => {
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 24, 15))
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 25, 14))

errorArg;
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 25, 14))

},
},
b: {
>b : Symbol(b, Decl(reverseMappedIntersectionInference.ts, 28, 4))

data: true,
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 29, 6))

onSuccess: (dataArg) => {
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 30, 15))
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 31, 16))

dataArg;
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 31, 16))

},
error: 500,
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 33, 6))

onError: (errorArg) => {
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 34, 15))
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 35, 14))

errorArg;
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 35, 14))

},
},
});

declare function withTuples<T extends any[], E extends any[]>(
>withTuples : Symbol(withTuples, Decl(reverseMappedIntersectionInference.ts, 39, 3))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 41, 28))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 41, 44))

arg: [...(Results<T> & Errors<E>)]
>arg : Symbol(arg, Decl(reverseMappedIntersectionInference.ts, 41, 62))
>Results : Symbol(Results, Decl(reverseMappedIntersectionInference.ts, 0, 0))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 41, 28))
>Errors : Symbol(Errors, Decl(reverseMappedIntersectionInference.ts, 5, 2))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 41, 44))

): [T, E];
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 41, 28))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 41, 44))

const res2 = withTuples([
>res2 : Symbol(res2, Decl(reverseMappedIntersectionInference.ts, 45, 5))
>withTuples : Symbol(withTuples, Decl(reverseMappedIntersectionInference.ts, 39, 3))
{
data: "foo",
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 46, 3))

onSuccess: (dataArg) => {
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 47, 16))
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 48, 16))

dataArg;
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 48, 16))

},
error: 404,
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 50, 6))

onError: (errorArg) => {
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 51, 15))
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 52, 14))

errorArg;
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 52, 14))

},
},
{
data: true,
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 56, 3))

onSuccess: (dataArg) => {
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 57, 15))
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 58, 16))

dataArg;
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 58, 16))

},
error: 500,
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 60, 6))

onError: (errorArg) => {
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 61, 15))
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 62, 14))

errorArg;
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 62, 14))

},
},
]);

type Tuple<T> = readonly [T, ...T[]];
>Tuple : Symbol(Tuple, Decl(reverseMappedIntersectionInference.ts, 66, 3))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 68, 11))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 68, 11))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 68, 11))

declare function withTuplesConstraints<T extends Tuple<any>, E extends Tuple<any>>(
>withTuplesConstraints : Symbol(withTuplesConstraints, Decl(reverseMappedIntersectionInference.ts, 68, 37))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 70, 39))
>Tuple : Symbol(Tuple, Decl(reverseMappedIntersectionInference.ts, 66, 3))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 70, 60))
>Tuple : Symbol(Tuple, Decl(reverseMappedIntersectionInference.ts, 66, 3))

arg: Results<T> & Errors<E>
>arg : Symbol(arg, Decl(reverseMappedIntersectionInference.ts, 70, 83))
>Results : Symbol(Results, Decl(reverseMappedIntersectionInference.ts, 0, 0))
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 70, 39))
>Errors : Symbol(Errors, Decl(reverseMappedIntersectionInference.ts, 5, 2))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 70, 60))

): [T, E];
>T : Symbol(T, Decl(reverseMappedIntersectionInference.ts, 70, 39))
>E : Symbol(E, Decl(reverseMappedIntersectionInference.ts, 70, 60))

const res3 = withTuplesConstraints([
>res3 : Symbol(res3, Decl(reverseMappedIntersectionInference.ts, 74, 5))
>withTuplesConstraints : Symbol(withTuplesConstraints, Decl(reverseMappedIntersectionInference.ts, 68, 37))
{
data: "foo",
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 75, 3))

onSuccess: (dataArg) => {
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 76, 16))
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 77, 16))

dataArg;
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 77, 16))

},
error: 404,
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 79, 6))

onError: (errorArg) => {
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 80, 15))
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 81, 14))

errorArg;
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 81, 14))

},
},
{
data: true,
>data : Symbol(data, Decl(reverseMappedIntersectionInference.ts, 85, 3))

onSuccess: (dataArg) => {
>onSuccess : Symbol(onSuccess, Decl(reverseMappedIntersectionInference.ts, 86, 15))
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 87, 16))

dataArg;
>dataArg : Symbol(dataArg, Decl(reverseMappedIntersectionInference.ts, 87, 16))

},
error: 500,
>error : Symbol(error, Decl(reverseMappedIntersectionInference.ts, 89, 6))

onError: (errorArg) => {
>onError : Symbol(onError, Decl(reverseMappedIntersectionInference.ts, 90, 15))
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 91, 14))

errorArg;
>errorArg : Symbol(errorArg, Decl(reverseMappedIntersectionInference.ts, 91, 14))

},
},
]);
Loading