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

Defer indexed access type resolution #17521

Merged
merged 6 commits into from Aug 7, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
111 changes: 83 additions & 28 deletions src/compiler/checker.ts
Expand Up @@ -2583,10 +2583,8 @@ namespace ts {
}

function createTypeNodeFromObjectType(type: ObjectType): TypeNode {
if (type.objectFlags & ObjectFlags.Mapped) {
if (getConstraintTypeFromMappedType(<MappedType>type).flags & (TypeFlags.TypeParameter | TypeFlags.Index)) {
return createMappedTypeNodeFromType(<MappedType>type);
}
if (isGenericMappedType(type)) {
return createMappedTypeNodeFromType(<MappedType>type);
}

const resolved = resolveStructuredTypeMembers(type);
Expand Down Expand Up @@ -3489,11 +3487,9 @@ namespace ts {
}

function writeLiteralType(type: ObjectType, flags: TypeFormatFlags) {
if (type.objectFlags & ObjectFlags.Mapped) {
if (getConstraintTypeFromMappedType(<MappedType>type).flags & (TypeFlags.TypeParameter | TypeFlags.Index)) {
writeMappedType(<MappedType>type);
return;
}
if (isGenericMappedType(type)) {
writeMappedType(<MappedType>type);
return;
}

const resolved = resolveStructuredTypeMembers(type);
Expand Down Expand Up @@ -5792,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 @@ -5905,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 @@ -5976,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 @@ -7602,26 +7608,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 Expand Up @@ -18657,6 +18710,8 @@ namespace ts {
}

function checkIndexedAccessType(node: IndexedAccessTypeNode) {
checkSourceElement(node.objectType);
checkSourceElement(node.indexType);
checkIndexedAccessIndexType(getTypeFromIndexedAccessTypeNode(node), node);
}

Expand Down
@@ -1,8 +1,11 @@
tests/cases/compiler/anyIndexedAccessArrayNoException.ts(1,12): error TS1122: A tuple type element list cannot be empty.
tests/cases/compiler/anyIndexedAccessArrayNoException.ts(1,12): error TS2538: Type '[]' cannot be used as an index type.


==== tests/cases/compiler/anyIndexedAccessArrayNoException.ts (1 errors) ====
==== tests/cases/compiler/anyIndexedAccessArrayNoException.ts (2 errors) ====
var x: any[[]];
~~
!!! error TS1122: A tuple type element list cannot be empty.
~~
!!! error TS2538: Type '[]' cannot be used as an index type.

64 changes: 64 additions & 0 deletions tests/baselines/reference/deferredLookupTypeResolution.js
@@ -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
@@ -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))
}