Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 36 additions & 12 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2136,7 +2136,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
/** Key is "/path/to/a.ts|/path/to/b.ts". */
var amalgamatedDuplicates: Map<string, DuplicateInfoForFiles> | undefined;
var reverseMappedCache = new Map<string, Type | undefined>();
var homomorphicMappedTypeInferenceStack: string[] = [];
var ambientModulesCache: Symbol[] | undefined;
/**
* List of every ambient module with a "*" wildcard.
Expand Down Expand Up @@ -2254,6 +2253,9 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
var potentialReflectCollisions: Node[] = [];
var potentialUnusedRenamedBindingElementsInTypes: BindingElement[] = [];
var awaitedTypeStack: number[] = [];
var reverseMappedSourceStack: Type[] = [];
var reverseMappedTargetStack: Type[] = [];
var reverseExpandingFlags = ExpandingFlags.None;

var diagnostics = createDiagnosticCollection();
var suggestionDiagnostics = createDiagnosticCollection();
Expand Down Expand Up @@ -14079,7 +14081,7 @@ 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) || unknownType, readonlyMask && indexInfo.isReadonly)] : emptyArray;
const members = createSymbolTable();
const limitedConstraint = getLimitedConstraint(type);
for (const prop of getPropertiesOfType(type.source)) {
Expand Down Expand Up @@ -25598,13 +25600,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (reverseMappedCache.has(cacheKey)) {
return reverseMappedCache.get(cacheKey);
}
const recursionKey = source.id + "," + (target.target || target).id;
if (contains(homomorphicMappedTypeInferenceStack, recursionKey)) {
return undefined;
}
homomorphicMappedTypeInferenceStack.push(recursionKey);
const type = createReverseMappedType(source, target, constraint);
homomorphicMappedTypeInferenceStack.pop();
reverseMappedCache.set(cacheKey, type);
return type;
}
Expand All @@ -25628,10 +25624,17 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// 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));
const elementType = inferReverseMappedType(getTypeArguments(source)[0], target, constraint);
if (!elementType) {
return undefined;
}
return createArrayType(elementType, isReadonlyArrayType(source));
}
if (isTupleType(source)) {
const elementTypes = map(getElementTypes(source), t => inferReverseMappedType(t, target, constraint));
if (!every(elementTypes, (t): t is Type => !!t)) {
return undefined;
}
const elementFlags = getMappedTypeModifiers(target) & MappedTypeModifiers.IncludeOptional ?
sameMap(source.target.elementFlags, f => f & ElementFlags.Optional ? ElementFlags.Required : f) :
source.target.elementFlags;
Expand All @@ -25646,22 +25649,43 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return reversed;
}

function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol) {
function getTypeOfReverseMappedSymbol(symbol: ReverseMappedSymbol): Type {
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) || unknownType;
}
return links.type;
}

function inferReverseMappedType(sourceType: Type, target: MappedType, constraint: IndexType): Type {
function inferReverseMappedTypeWorker(sourceType: Type, target: MappedType, constraint: IndexType): Type {
const typeParameter = getIndexedAccessType(constraint.type, getTypeParameterFromMappedType(target)) as TypeParameter;
const templateType = getTemplateTypeFromMappedType(target);
const inference = createInferenceInfo(typeParameter);
inferTypes([inference], sourceType, templateType);
return getTypeFromInference(inference) || unknownType;
}

function inferReverseMappedType(source: Type, target: MappedType, constraint: IndexType): Type | undefined {
const cacheKey = source.id + "," + target.id + "," + constraint.id;
if (reverseMappedCache.has(cacheKey)) {
return reverseMappedCache.get(cacheKey) || unknownType;
}
reverseMappedSourceStack.push(source);
reverseMappedTargetStack.push(target);
const saveExpandingFlags = reverseExpandingFlags;
if (isDeeplyNestedType(source, reverseMappedSourceStack, reverseMappedSourceStack.length, 2)) reverseExpandingFlags |= ExpandingFlags.Source;
if (isDeeplyNestedType(target, reverseMappedTargetStack, reverseMappedTargetStack.length, 2)) reverseExpandingFlags |= ExpandingFlags.Target;
let type;
if (reverseExpandingFlags !== ExpandingFlags.Both) {
type = inferReverseMappedTypeWorker(source, target, constraint);
}
reverseMappedSourceStack.pop();
reverseMappedTargetStack.pop();
reverseExpandingFlags = saveExpandingFlags;
reverseMappedCache.set(cacheKey, type);
return type;
}

function* getUnmatchedProperties(source: Type, target: Type, requireOptionalProperties: boolean, matchDiscriminantProperties: boolean): IterableIterator<Symbol> {
const properties = getPropertiesOfType(target);
for (const targetProp of properties) {
Expand Down
18 changes: 10 additions & 8 deletions tests/baselines/reference/mappedTypeRecursiveInference.errors.txt

Large diffs are not rendered by default.

10 changes: 5 additions & 5 deletions tests/baselines/reference/mappedTypeRecursiveInference.types
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//// [tests/cases/compiler/mappedTypeRecursiveInference.ts] ////

=== Performance Stats ===
Strict subtype cache: 300 / 300 (nearest 100)
Assignability cache: 5,000 / 5,000 (nearest 100)
Type Count: 15,000 / 15,000 (nearest 100)
Instantiation count: 486,000 / 503,500 (nearest 500)
Symbol count: 174,500 / 177,000 (nearest 500)
Strict subtype cache: 100 / 100 (nearest 100)
Assignability cache: 4,100 / 4,100 (nearest 100)
Type Count: 12,200 / 12,200 (nearest 100)
Instantiation count: 341,000 / 358,500 (nearest 500)
Symbol count: 117,500 / 120,000 (nearest 500)

=== mappedTypeRecursiveInference.ts ===
interface A { a: A }
Expand Down
28 changes: 28 additions & 0 deletions tests/baselines/reference/reverseMappedTypeRecursiveInference.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//// [tests/cases/compiler/reverseMappedTypeRecursiveInference.ts] ////

//// [reverseMappedTypeRecursiveInference.ts]
type Foo<V> = {
[K in keyof V]: Foo<V[K]>;
}

type Bar<V> = {
[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
}

function test<V>(value: Foo<V>): V {
console.log(value);
return undefined as any;
}

const bar: Bar<any> = {};

test(bar);

//// [reverseMappedTypeRecursiveInference.js]
"use strict";
function test(value) {
console.log(value);
return undefined;
}
var bar = {};
test(bar);
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//// [tests/cases/compiler/reverseMappedTypeRecursiveInference.ts] ////

=== reverseMappedTypeRecursiveInference.ts ===
type Foo<V> = {
>Foo : Symbol(Foo, Decl(reverseMappedTypeRecursiveInference.ts, 0, 0))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 0, 9))

[K in keyof V]: Foo<V[K]>;
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 1, 5))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 0, 9))
>Foo : Symbol(Foo, Decl(reverseMappedTypeRecursiveInference.ts, 0, 0))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 0, 9))
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 1, 5))
}

type Bar<V> = {
>Bar : Symbol(Bar, Decl(reverseMappedTypeRecursiveInference.ts, 2, 1))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))

[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 5, 5))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 5, 5))
>Bar : Symbol(Bar, Decl(reverseMappedTypeRecursiveInference.ts, 2, 1))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 4, 9))
>K : Symbol(K, Decl(reverseMappedTypeRecursiveInference.ts, 5, 5))
}

function test<V>(value: Foo<V>): V {
>test : Symbol(test, Decl(reverseMappedTypeRecursiveInference.ts, 6, 1))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 8, 14))
>value : Symbol(value, Decl(reverseMappedTypeRecursiveInference.ts, 8, 17))
>Foo : Symbol(Foo, Decl(reverseMappedTypeRecursiveInference.ts, 0, 0))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 8, 14))
>V : Symbol(V, Decl(reverseMappedTypeRecursiveInference.ts, 8, 14))

console.log(value);
>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>console : Symbol(console, Decl(lib.dom.d.ts, --, --))
>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --))
>value : Symbol(value, Decl(reverseMappedTypeRecursiveInference.ts, 8, 17))

return undefined as any;
>undefined : Symbol(undefined)
}

const bar: Bar<any> = {};
>bar : Symbol(bar, Decl(reverseMappedTypeRecursiveInference.ts, 13, 5))
>Bar : Symbol(Bar, Decl(reverseMappedTypeRecursiveInference.ts, 2, 1))

test(bar);
>test : Symbol(test, Decl(reverseMappedTypeRecursiveInference.ts, 6, 1))
>bar : Symbol(bar, Decl(reverseMappedTypeRecursiveInference.ts, 13, 5))

Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
//// [tests/cases/compiler/reverseMappedTypeRecursiveInference.ts] ////

=== reverseMappedTypeRecursiveInference.ts ===
type Foo<V> = {
>Foo : Foo<V>
> : ^^^^^^

[K in keyof V]: Foo<V[K]>;
}

type Bar<V> = {
>Bar : Bar<V>
> : ^^^^^^

[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
}

function test<V>(value: Foo<V>): V {
>test : <V>(value: Foo<V>) => V
> : ^ ^^ ^^ ^^^^^
>value : Foo<V>
> : ^^^^^^

console.log(value);
>console.log(value) : void
> : ^^^^
>console.log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^^^^^
>console : Console
> : ^^^^^^^
>log : (...data: any[]) => void
> : ^^^^ ^^ ^^^^^^^^^
>value : Foo<V>
> : ^^^^^^

return undefined as any;
>undefined as any : any
>undefined : undefined
> : ^^^^^^^^^
}

const bar: Bar<any> = {};
>bar : Bar<any>
> : ^^^^^^^^
>{} : {}
> : ^^

test(bar);
>test(bar) : { [x: string]: any; }
> : ^^^^^^^^^^^^^^^^^^^^^
>test : <V>(value: Foo<V>) => V
> : ^ ^^ ^^ ^^^^^^
>bar : Bar<any>
> : ^^^^^^^^

18 changes: 18 additions & 0 deletions tests/cases/compiler/reverseMappedTypeRecursiveInference.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// @strict: true

type Foo<V> = {
[K in keyof V]: Foo<V[K]>;
}

type Bar<V> = {
[K in keyof V]: V[K] extends object ? Bar<V[K]> : string;
}

function test<V>(value: Foo<V>): V {
console.log(value);
return undefined as any;
}

const bar: Bar<any> = {};

test(bar);