Skip to content

Commit

Permalink
Merge pull request #17521 from Microsoft/deferLookupTypeResolution
Browse files Browse the repository at this point in the history
Defer indexed access type resolution
  • Loading branch information
ahejlsberg authored Aug 7, 2017
2 parents 48d5485 + 98f6761 commit 313c93c
Show file tree
Hide file tree
Showing 8 changed files with 433 additions and 19 deletions.
95 changes: 76 additions & 19 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5788,8 +5788,7 @@ namespace ts {
}

function isGenericMappedType(type: Type) {
return getObjectFlags(type) & ObjectFlags.Mapped &&
maybeTypeOfKind(getConstraintTypeFromMappedType(<MappedType>type), TypeFlags.TypeVariable | TypeFlags.Index);
return getObjectFlags(type) & ObjectFlags.Mapped && isGenericIndexType(getConstraintTypeFromMappedType(<MappedType>type));
}

function resolveStructuredTypeMembers(type: StructuredType): ResolvedType {
Expand Down Expand Up @@ -5901,6 +5900,10 @@ namespace ts {
}

function getConstraintOfIndexedAccess(type: IndexedAccessType) {
const transformed = getTransformedIndexedAccessType(type);
if (transformed) {
return transformed;
}
const baseObjectType = getBaseConstraintOfType(type.objectType);
const baseIndexType = getBaseConstraintOfType(type.indexType);
return baseObjectType || baseIndexType ? getIndexedAccessType(baseObjectType || type.objectType, baseIndexType || type.indexType) : undefined;
Expand Down Expand Up @@ -5972,11 +5975,18 @@ namespace ts {
return stringType;
}
if (t.flags & TypeFlags.IndexedAccess) {
const transformed = getTransformedIndexedAccessType(<IndexedAccessType>t);
if (transformed) {
return getBaseConstraint(transformed);
}
const baseObjectType = getBaseConstraint((<IndexedAccessType>t).objectType);
const baseIndexType = getBaseConstraint((<IndexedAccessType>t).indexType);
const baseIndexedAccess = baseObjectType && baseIndexType ? getIndexedAccessType(baseObjectType, baseIndexType) : undefined;
return baseIndexedAccess && baseIndexedAccess !== unknownType ? getBaseConstraint(baseIndexedAccess) : undefined;
}
if (isGenericMappedType(t)) {
return emptyObjectType;
}
return t;
}
}
Expand Down Expand Up @@ -7604,26 +7614,73 @@ namespace ts {
return instantiateType(getTemplateTypeFromMappedType(type), templateMapper);
}

function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode) {
// If the index type is generic, if the object type is generic and doesn't originate in an expression,
// or if the object type is a mapped type with a generic constraint, we are performing a higher-order
// index access where we cannot meaningfully access the properties of the object type. Note that for a
// generic T and a non-generic K, we eagerly resolve T[K] if it originates in an expression. This is to
// preserve backwards compatibility. For example, an element access 'this["foo"]' has always been resolved
// eagerly using the constraint type of 'this' at the given location.
if (maybeTypeOfKind(indexType, TypeFlags.TypeVariable | TypeFlags.Index) ||
maybeTypeOfKind(objectType, TypeFlags.TypeVariable) && !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) ||
isGenericMappedType(objectType)) {
function isGenericObjectType(type: Type): boolean {
return type.flags & TypeFlags.TypeVariable ? true :
getObjectFlags(type) & ObjectFlags.Mapped ? isGenericIndexType(getConstraintTypeFromMappedType(<MappedType>type)) :
type.flags & TypeFlags.UnionOrIntersection ? forEach((<UnionOrIntersectionType>type).types, isGenericObjectType) :
false;
}

function isGenericIndexType(type: Type): boolean {
return type.flags & (TypeFlags.TypeVariable | TypeFlags.Index) ? true :
type.flags & TypeFlags.UnionOrIntersection ? forEach((<UnionOrIntersectionType>type).types, isGenericIndexType) :
false;
}

// Return true if the given type is a non-generic object type with a string index signature and no
// other members.
function isStringIndexOnlyType(type: Type) {
if (type.flags & TypeFlags.Object && !isGenericMappedType(type)) {
const t = resolveStructuredTypeMembers(<ObjectType>type);
return t.properties.length === 0 &&
t.callSignatures.length === 0 && t.constructSignatures.length === 0 &&
t.stringIndexInfo && !t.numberIndexInfo;
}
return false;
}

// Given an indexed access type T[K], if T is an intersection containing one or more generic types and one or
// more object types with only a string index signature, e.g. '(U & V & { [x: string]: D })[K]', return a
// transformed type of the form '(U & V)[K] | D'. This allows us to properly reason about higher order indexed
// access types with default property values as expressed by D.
function getTransformedIndexedAccessType(type: IndexedAccessType): Type {
const objectType = type.objectType;
if (objectType.flags & TypeFlags.Intersection && isGenericObjectType(objectType) && some((<IntersectionType>objectType).types, isStringIndexOnlyType)) {
const regularTypes: Type[] = [];
const stringIndexTypes: Type[] = [];
for (const t of (<IntersectionType>objectType).types) {
if (isStringIndexOnlyType(t)) {
stringIndexTypes.push(getIndexTypeOfType(t, IndexKind.String));
}
else {
regularTypes.push(t);
}
}
return getUnionType([
getIndexedAccessType(getIntersectionType(regularTypes), type.indexType),
getIntersectionType(stringIndexTypes)
]);
}
return undefined;
}

function getIndexedAccessType(objectType: Type, indexType: Type, accessNode?: ElementAccessExpression | IndexedAccessTypeNode): Type {
// If the object type is a mapped type { [P in K]: E }, where K is generic, we 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
// construct the type Box<T[X]>.
if (isGenericMappedType(objectType)) {
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
}
// Otherwise, if the index type is generic, or if the object type is generic and doesn't originate in an
// expression, we are performing a higher-order index access where we cannot meaningfully access the properties
// of the object type. Note that for a generic T and a non-generic K, we eagerly resolve T[K] if it originates
// in an expression. This is to preserve backwards compatibility. For example, an element access 'this["foo"]'
// has always been resolved eagerly using the constraint type of 'this' at the given location.
if (isGenericIndexType(indexType) || !(accessNode && accessNode.kind === SyntaxKind.ElementAccessExpression) && isGenericObjectType(objectType)) {
if (objectType.flags & TypeFlags.Any) {
return objectType;
}
// If the object type is a mapped type { [P in K]: E }, we 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 construct the
// type Box<T[X]>.
if (isGenericMappedType(objectType)) {
return getIndexedAccessForMappedType(<MappedType>objectType, indexType, accessNode);
}
// Otherwise we defer the operation by creating an indexed access type.
// Defer the operation by creating an indexed access type.
const id = objectType.id + "," + indexType.id;
let type = indexedAccessTypes.get(id);
if (!type) {
Expand Down
64 changes: 64 additions & 0 deletions tests/baselines/reference/deferredLookupTypeResolution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
//// [deferredLookupTypeResolution.ts]
// Repro from #17456

type StringContains<S extends string, L extends string> = (
{ [K in S]: 'true' } &
{ [key: string]: 'false' }
)[L]

type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>

type First<T> = ObjectHasKey<T, '0'>; // Should be deferred

type T1 = ObjectHasKey<{ a: string }, 'a'>; // 'true'
type T2 = ObjectHasKey<{ a: string }, 'b'>; // 'false'

// Verify that mapped type isn't eagerly resolved in type-to-string operation

declare function f1<A extends string, B extends string>(a: A, b: B): { [P in A | B]: any };

function f2<A extends string>(a: A) {
return f1(a, 'x');
}

function f3(x: 'a' | 'b') {
return f2(x);
}


//// [deferredLookupTypeResolution.js]
"use strict";
// Repro from #17456
function f2(a) {
return f1(a, 'x');
}
function f3(x) {
return f2(x);
}


//// [deferredLookupTypeResolution.d.ts]
declare type StringContains<S extends string, L extends string> = ({
[K in S]: 'true';
} & {
[key: string]: 'false';
})[L];
declare type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>;
declare type First<T> = ObjectHasKey<T, '0'>;
declare type T1 = ObjectHasKey<{
a: string;
}, 'a'>;
declare type T2 = ObjectHasKey<{
a: string;
}, 'b'>;
declare function f1<A extends string, B extends string>(a: A, b: B): {
[P in A | B]: any;
};
declare function f2<A extends string>(a: A): {
[P in A | "x"]: any;
};
declare function f3(x: 'a' | 'b'): {
a: any;
b: any;
x: any;
};
76 changes: 76 additions & 0 deletions tests/baselines/reference/deferredLookupTypeResolution.symbols
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
=== tests/cases/compiler/deferredLookupTypeResolution.ts ===
// Repro from #17456

type StringContains<S extends string, L extends string> = (
>StringContains : Symbol(StringContains, Decl(deferredLookupTypeResolution.ts, 0, 0))
>S : Symbol(S, Decl(deferredLookupTypeResolution.ts, 2, 20))
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 2, 37))

{ [K in S]: 'true' } &
>K : Symbol(K, Decl(deferredLookupTypeResolution.ts, 3, 7))
>S : Symbol(S, Decl(deferredLookupTypeResolution.ts, 2, 20))

{ [key: string]: 'false' }
>key : Symbol(key, Decl(deferredLookupTypeResolution.ts, 4, 7))

)[L]
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 2, 37))

type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
>O : Symbol(O, Decl(deferredLookupTypeResolution.ts, 7, 18))
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 7, 20))
>StringContains : Symbol(StringContains, Decl(deferredLookupTypeResolution.ts, 0, 0))
>O : Symbol(O, Decl(deferredLookupTypeResolution.ts, 7, 18))
>L : Symbol(L, Decl(deferredLookupTypeResolution.ts, 7, 20))

type First<T> = ObjectHasKey<T, '0'>; // Should be deferred
>First : Symbol(First, Decl(deferredLookupTypeResolution.ts, 7, 67))
>T : Symbol(T, Decl(deferredLookupTypeResolution.ts, 9, 11))
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
>T : Symbol(T, Decl(deferredLookupTypeResolution.ts, 9, 11))

type T1 = ObjectHasKey<{ a: string }, 'a'>; // 'true'
>T1 : Symbol(T1, Decl(deferredLookupTypeResolution.ts, 9, 37))
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 11, 24))

type T2 = ObjectHasKey<{ a: string }, 'b'>; // 'false'
>T2 : Symbol(T2, Decl(deferredLookupTypeResolution.ts, 11, 43))
>ObjectHasKey : Symbol(ObjectHasKey, Decl(deferredLookupTypeResolution.ts, 5, 6))
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 12, 24))

// Verify that mapped type isn't eagerly resolved in type-to-string operation

declare function f1<A extends string, B extends string>(a: A, b: B): { [P in A | B]: any };
>f1 : Symbol(f1, Decl(deferredLookupTypeResolution.ts, 12, 43))
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 16, 20))
>B : Symbol(B, Decl(deferredLookupTypeResolution.ts, 16, 37))
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 16, 56))
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 16, 20))
>b : Symbol(b, Decl(deferredLookupTypeResolution.ts, 16, 61))
>B : Symbol(B, Decl(deferredLookupTypeResolution.ts, 16, 37))
>P : Symbol(P, Decl(deferredLookupTypeResolution.ts, 16, 72))
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 16, 20))
>B : Symbol(B, Decl(deferredLookupTypeResolution.ts, 16, 37))

function f2<A extends string>(a: A) {
>f2 : Symbol(f2, Decl(deferredLookupTypeResolution.ts, 16, 91))
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 18, 12))
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 18, 30))
>A : Symbol(A, Decl(deferredLookupTypeResolution.ts, 18, 12))

return f1(a, 'x');
>f1 : Symbol(f1, Decl(deferredLookupTypeResolution.ts, 12, 43))
>a : Symbol(a, Decl(deferredLookupTypeResolution.ts, 18, 30))
}

function f3(x: 'a' | 'b') {
>f3 : Symbol(f3, Decl(deferredLookupTypeResolution.ts, 20, 1))
>x : Symbol(x, Decl(deferredLookupTypeResolution.ts, 22, 12))

return f2(x);
>f2 : Symbol(f2, Decl(deferredLookupTypeResolution.ts, 16, 91))
>x : Symbol(x, Decl(deferredLookupTypeResolution.ts, 22, 12))
}

79 changes: 79 additions & 0 deletions tests/baselines/reference/deferredLookupTypeResolution.types
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
=== tests/cases/compiler/deferredLookupTypeResolution.ts ===
// Repro from #17456

type StringContains<S extends string, L extends string> = (
>StringContains : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
>S : S
>L : L

{ [K in S]: 'true' } &
>K : K
>S : S

{ [key: string]: 'false' }
>key : string

)[L]
>L : L

type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
>O : O
>L : L
>StringContains : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
>O : O
>L : L

type First<T> = ObjectHasKey<T, '0'>; // Should be deferred
>First : ({ [K in S]: "true"; } & { [key: string]: "false"; })["0"]
>T : T
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
>T : T

type T1 = ObjectHasKey<{ a: string }, 'a'>; // 'true'
>T1 : "true"
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
>a : string

type T2 = ObjectHasKey<{ a: string }, 'b'>; // 'false'
>T2 : "false"
>ObjectHasKey : ({ [K in S]: "true"; } & { [key: string]: "false"; })[L]
>a : string

// Verify that mapped type isn't eagerly resolved in type-to-string operation

declare function f1<A extends string, B extends string>(a: A, b: B): { [P in A | B]: any };
>f1 : <A extends string, B extends string>(a: A, b: B) => { [P in A | B]: any; }
>A : A
>B : B
>a : A
>A : A
>b : B
>B : B
>P : P
>A : A
>B : B

function f2<A extends string>(a: A) {
>f2 : <A extends string>(a: A) => { [P in A | B]: any; }
>A : A
>a : A
>A : A

return f1(a, 'x');
>f1(a, 'x') : { [P in A | B]: any; }
>f1 : <A extends string, B extends string>(a: A, b: B) => { [P in A | B]: any; }
>a : A
>'x' : "x"
}

function f3(x: 'a' | 'b') {
>f3 : (x: "a" | "b") => { a: any; b: any; x: any; }
>x : "a" | "b"

return f2(x);
>f2(x) : { a: any; b: any; x: any; }
>f2 : <A extends string>(a: A) => { [P in A | B]: any; }
>x : "a" | "b"
}

31 changes: 31 additions & 0 deletions tests/baselines/reference/deferredLookupTypeResolution2.errors.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
tests/cases/compiler/deferredLookupTypeResolution2.ts(14,13): error TS2536: Type '({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]' cannot be used to index type '{ true: "true"; }'.
tests/cases/compiler/deferredLookupTypeResolution2.ts(19,21): error TS2536: Type '({ true: "otherwise"; } & { [k: string]: "true"; })[({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]]' cannot be used to index type '{ true: "true"; }'.


==== tests/cases/compiler/deferredLookupTypeResolution2.ts (2 errors) ====
// Repro from #17456

type StringContains<S extends string, L extends string> = ({ [K in S]: 'true' } & { [key: string]: 'false'})[L];

type ObjectHasKey<O, L extends string> = StringContains<keyof O, L>;

type A<T> = ObjectHasKey<T, '0'>;

type B = ObjectHasKey<[string, number], '1'>; // "true"
type C = ObjectHasKey<[string, number], '2'>; // "false"
type D = A<[string]>; // "true"

// Error, "false" not handled
type E<T> = { true: 'true' }[ObjectHasKey<T, '1'>];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2536: Type '({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]' cannot be used to index type '{ true: "true"; }'.

type Juxtapose<T> = ({ true: 'otherwise' } & { [k: string]: 'true' })[ObjectHasKey<T, '1'>];

// Error, "otherwise" is missing
type DeepError<T> = { true: 'true' }[Juxtapose<T>];
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
!!! error TS2536: Type '({ true: "otherwise"; } & { [k: string]: "true"; })[({ [K in S]: "true"; } & { [key: string]: "false"; })["1"]]' cannot be used to index type '{ true: "true"; }'.

type DeepOK<T> = { true: 'true', otherwise: 'false' }[Juxtapose<T>];

Loading

0 comments on commit 313c93c

Please sign in to comment.