Skip to content

Add transformation to indexing intersection of homomorphic mapepd type and generic parameter #25490

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
35 changes: 35 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8999,6 +8999,14 @@ namespace ts {
return !!(getObjectFlags(type) & ObjectFlags.Mapped) && getTemplateTypeFromMappedType(type as MappedType) === neverType;
}

function isMappedTypeOverKeyofGenericInSet(set: ReadonlyArray<Type>): (type: Type) => boolean {
return (type) => {
if (!(getObjectFlags(type) & ObjectFlags.Mapped)) return false;
const constraint = getConstraintTypeFromMappedType(type as MappedType);
return some(set, t => getIndexType(t) === constraint);
};
}

function getSimplifiedType(type: Type): Type {
return type.flags & TypeFlags.IndexedAccess ? getSimplifiedIndexedAccessType(<IndexedAccessType>type) : type;
}
Expand Down Expand Up @@ -9042,6 +9050,33 @@ namespace ts {
const nonNeverTypes = filter((<IntersectionType>objectType).types, t => !isMappedTypeToNever(t));
return type.simplified = getSimplifiedType(getIndexedAccessType(getIntersectionType(nonNeverTypes), type.indexType));
}

// Given an indexed access type T[K], if T is an intersection containing a generic type and one or
// more mapped types which map over the keys of that generic, '(T & {[P in keyof T]: Foo})[K]', return a
// transformed type that moves the top-level intersection into the mapped type: '{[P in keyof T]: T[P] & Foo}[K]'
// If we don't do such a transformation now, due to how we reason over intersections, we will never come to the
// realization that the member we're trying to pluck out is influenced by both `T[P]` _and_ the mapped type template
const isInSet = isMappedTypeOverKeyofGenericInSet((<IntersectionType>objectType).types);
if (some((<IntersectionType>objectType).types, isInSet)) {
Copy link
Member

Choose a reason for hiding this comment

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

Could you turn this into a findIndex and then avoid the inner loop?

for (const t of (<IntersectionType>objectType).types) {
if (isInSet(t)) {
const constraint = getConstraintTypeFromMappedType(t as MappedType);
let matchIndex: number | undefined;
const match = forEach((<IntersectionType>objectType).types, (t, i) => getIndexType(t) === constraint ? (matchIndex = i, t) : undefined)!;
const indexedMatch = getIndexedAccessType(match, getTypeParameterFromMappedType(t as MappedType));
const newTemaplate = getIntersectionType([indexedMatch, getTemplateTypeFromMappedType(t as MappedType)]);
const newMappedType = <MappedType>createObjectType(ObjectFlags.Mapped, t.symbol);
newMappedType.declaration = (t as MappedType).declaration;
newMappedType.templateType = newTemaplate;
newMappedType.typeParameter = (t as MappedType).typeParameter;
newMappedType.constraintType = (t as MappedType).constraintType;
const newSet = (<IntersectionType>objectType).types.slice();
newSet.splice(matchIndex!, 1); // Remove the generic
Copy link
Member

Choose a reason for hiding this comment

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

orderedRemoveItemAt (or unorderedRemoveItemAt if it doesn't matter)

newSet.splice(newSet.indexOf(t), 1, newMappedType); // Remove the original mapped type
Copy link
Member

@DanielRosenwasser DanielRosenwasser Jul 7, 2018

Choose a reason for hiding this comment

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

Why not directly overwrite the original location?

Copy link
Member Author

Choose a reason for hiding this comment

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

... fair point. I was in a splicy mood, I suppose.

Copy link
Member

Choose a reason for hiding this comment

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

🔥 🔥 🔥 🔥

return type.simplified = getSimplifiedType(getIndexedAccessType(getIntersectionType(newSet), type.indexType));
}
}
}
}
// If the object type is a mapped type { [P in K]: E }, where K is generic, instantiate E using a mapper
// that substitutes the index type for P. For example, for an index access { [P in K]: Box<T[P]> }[X], we
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts(20,32): error TS2339: Property 'returnValue' does not exist on type 'Function'.


==== tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts (1 errors) ====
interface Spy {
(...params: any[]): any;

identity: string;
and: Function;
mostRecentCall: { args: any[]; };
argsForCall: any[];
}

type SpyObj<T> = T & {
[k in keyof T]: Spy;
}

declare function createSpyObj<T>(
name: string, names: Array<keyof T>): SpyObj<T>;

function mock<T>(spyName: string, methodNames: Array<keyof T>): SpyObj<T> {
const spyObj = createSpyObj<T>(spyName, methodNames);
for (const methodName of methodNames) {
spyObj[methodName].and.returnValue(1);
~~~~~~~~~~~
!!! error TS2339: Property 'returnValue' does not exist on type 'Function'.
}
return spyObj;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//// [intersectionMembersOfMappedTypeHaveCorrectApparentType.ts]
interface Spy {
(...params: any[]): any;

identity: string;
and: Function;
mostRecentCall: { args: any[]; };
argsForCall: any[];
}

type SpyObj<T> = T & {
[k in keyof T]: Spy;
}

declare function createSpyObj<T>(
name: string, names: Array<keyof T>): SpyObj<T>;

function mock<T>(spyName: string, methodNames: Array<keyof T>): SpyObj<T> {
const spyObj = createSpyObj<T>(spyName, methodNames);
for (const methodName of methodNames) {
spyObj[methodName].and.returnValue(1);
}
return spyObj;
}

//// [intersectionMembersOfMappedTypeHaveCorrectApparentType.js]
function mock(spyName, methodNames) {
var spyObj = createSpyObj(spyName, methodNames);
for (var _i = 0, methodNames_1 = methodNames; _i < methodNames_1.length; _i++) {
var methodName = methodNames_1[_i];
spyObj[methodName].and.returnValue(1);
}
return spyObj;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
=== tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts ===
interface Spy {
>Spy : Symbol(Spy, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 0, 0))

(...params: any[]): any;
>params : Symbol(params, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 1, 5))

identity: string;
>identity : Symbol(Spy.identity, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 1, 28))

and: Function;
>and : Symbol(Spy.and, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 3, 21))
>Function : Symbol(Function, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))

mostRecentCall: { args: any[]; };
>mostRecentCall : Symbol(Spy.mostRecentCall, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 4, 18))
>args : Symbol(args, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 5, 21))

argsForCall: any[];
>argsForCall : Symbol(Spy.argsForCall, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 5, 37))
}

type SpyObj<T> = T & {
>SpyObj : Symbol(SpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 7, 1))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 9, 12))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 9, 12))

[k in keyof T]: Spy;
>k : Symbol(k, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 10, 5))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 9, 12))
>Spy : Symbol(Spy, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 0, 0))
}

declare function createSpyObj<T>(
>createSpyObj : Symbol(createSpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 11, 1))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 30))

name: string, names: Array<keyof T>): SpyObj<T>;
>name : Symbol(name, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 33))
>names : Symbol(names, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 14, 17))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 30))
>SpyObj : Symbol(SpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 7, 1))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 13, 30))

function mock<T>(spyName: string, methodNames: Array<keyof T>): SpyObj<T> {
>mock : Symbol(mock, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 14, 52))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14))
>spyName : Symbol(spyName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 17))
>methodNames : Symbol(methodNames, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 33))
>Array : Symbol(Array, Decl(lib.es5.d.ts, --, --), Decl(lib.es5.d.ts, --, --))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14))
>SpyObj : Symbol(SpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 7, 1))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14))

const spyObj = createSpyObj<T>(spyName, methodNames);
>spyObj : Symbol(spyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 17, 9))
>createSpyObj : Symbol(createSpyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 11, 1))
>T : Symbol(T, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 14))
>spyName : Symbol(spyName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 17))
>methodNames : Symbol(methodNames, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 33))

for (const methodName of methodNames) {
>methodName : Symbol(methodName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 18, 14))
>methodNames : Symbol(methodNames, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 16, 33))

spyObj[methodName].and.returnValue(1);
>spyObj[methodName].and : Symbol(Spy.and, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 3, 21))
>spyObj : Symbol(spyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 17, 9))
>methodName : Symbol(methodName, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 18, 14))
>and : Symbol(Spy.and, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 3, 21))
}
return spyObj;
>spyObj : Symbol(spyObj, Decl(intersectionMembersOfMappedTypeHaveCorrectApparentType.ts, 17, 9))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
=== tests/cases/compiler/intersectionMembersOfMappedTypeHaveCorrectApparentType.ts ===
interface Spy {
>Spy : Spy

(...params: any[]): any;
>params : any[]

identity: string;
>identity : string

and: Function;
>and : Function
>Function : Function

mostRecentCall: { args: any[]; };
>mostRecentCall : { args: any[]; }
>args : any[]

argsForCall: any[];
>argsForCall : any[]
}

type SpyObj<T> = T & {
>SpyObj : SpyObj<T>
>T : T
>T : T

[k in keyof T]: Spy;
>k : k
>T : T
>Spy : Spy
}

declare function createSpyObj<T>(
>createSpyObj : <T>(name: string, names: (keyof T)[]) => SpyObj<T>
>T : T

name: string, names: Array<keyof T>): SpyObj<T>;
>name : string
>names : (keyof T)[]
>Array : T[]
>T : T
>SpyObj : SpyObj<T>
>T : T

function mock<T>(spyName: string, methodNames: Array<keyof T>): SpyObj<T> {
>mock : <T>(spyName: string, methodNames: (keyof T)[]) => SpyObj<T>
>T : T
>spyName : string
>methodNames : (keyof T)[]
>Array : T[]
>T : T
>SpyObj : SpyObj<T>
>T : T

const spyObj = createSpyObj<T>(spyName, methodNames);
>spyObj : SpyObj<T>
>createSpyObj<T>(spyName, methodNames) : SpyObj<T>
>createSpyObj : <T>(name: string, names: (keyof T)[]) => SpyObj<T>
>T : T
>spyName : string
>methodNames : (keyof T)[]

for (const methodName of methodNames) {
>methodName : keyof T
>methodNames : (keyof T)[]

spyObj[methodName].and.returnValue(1);
>spyObj[methodName].and.returnValue(1) : any
>spyObj[methodName].and.returnValue : any
>spyObj[methodName].and : Function
>spyObj[methodName] : SpyObj<T>[keyof T]
>spyObj : SpyObj<T>
>methodName : keyof T
>and : Function
>returnValue : any
>1 : 1
}
return spyObj;
>spyObj : SpyObj<T>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
interface Spy {
(...params: any[]): any;

identity: string;
and: Function;
mostRecentCall: { args: any[]; };
argsForCall: any[];
}

type SpyObj<T> = T & {
[k in keyof T]: Spy;
}

declare function createSpyObj<T>(
name: string, names: Array<keyof T>): SpyObj<T>;

function mock<T>(spyName: string, methodNames: Array<keyof T>): SpyObj<T> {
const spyObj = createSpyObj<T>(spyName, methodNames);
for (const methodName of methodNames) {
spyObj[methodName].and.returnValue(1);
}
return spyObj;
}