Skip to content

Commit de79f4a

Browse files
committed
Preserve reverse mapped types in declaration emit
Display placeholders discard real property and index signature types when reused for declaration emit. Preserve those types in declarations while leaving editor display unchanged. Reverse-mapped types without symbols can still recurse. Track their type identities and bound expansion by mapped declaration when changing type arguments prevent repetition. Report cycles and truncation rather than silently replacing these types with any.
1 parent ee197e0 commit de79f4a

4 files changed

Lines changed: 168 additions & 58 deletions

File tree

‎tsc/internal/checker/nodebuilderimpl.go‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,6 +2372,10 @@ func (b *NodeBuilderImpl) serializeTypeForDeclaration(declaration *ast.Declarati
23722372
const MAX_REVERSE_MAPPED_NESTING_INSPECTION_DEPTH = 3
23732373

23742374
func (b *NodeBuilderImpl) shouldUsePlaceholderForProperty(propertySymbol *ast.Symbol) bool {
2375+
// Reverse mapped type placeholders are for display, not declaration emit.
2376+
if b.ctx.flags&nodebuilder.FlagsAllowAnonymousIdentifier == 0 {
2377+
return false
2378+
}
23752379
// Use placeholders for reverse mapped types we've either
23762380
// (1) already descended into, or
23772381
// (2) are nested reverse mappings within a mapping over a non-anonymous type, or
@@ -2735,7 +2739,11 @@ func (b *NodeBuilderImpl) createTypeNodesFromResolvedType(resolvedType *Structur
27352739
typeElements = append(typeElements, b.signatureToSignatureDeclarationHelper(signature, ast.KindConstructSignature, nil))
27362740
}
27372741
for _, info := range resolvedType.indexInfos {
2738-
typeElements = slices.Concat(typeElements, b.indexInfoToObjectComputedNamesOrSignatureDeclaration(info, core.IfElse(resolvedType.objectFlags&ObjectFlagsReverseMapped != 0, b.createElidedInformationPlaceholder(), nil)))
2742+
var typeNode *ast.TypeNode
2743+
if resolvedType.objectFlags&ObjectFlagsReverseMapped != 0 && b.ctx.flags&nodebuilder.FlagsAllowAnonymousIdentifier != 0 {
2744+
typeNode = b.createElidedInformationPlaceholder()
2745+
}
2746+
typeElements = slices.Concat(typeElements, b.indexInfoToObjectComputedNamesOrSignatureDeclaration(info, typeNode))
27392747
}
27402748

27412749
properties := resolvedType.properties
@@ -2969,8 +2977,13 @@ func (b *NodeBuilderImpl) createAnonymousTypeNodeEx(t *Type, forceClassExpansion
29692977
} else {
29702978
return b.visitAndTransformType(t, (*NodeBuilderImpl).createTypeNodeFromObjectType)
29712979
}
2980+
} else if t.objectFlags&ObjectFlagsReverseMapped != 0 && b.ctx.flags&nodebuilder.FlagsAllowAnonymousIdentifier == 0 {
2981+
if b.ctx.visitedTypes.Has(typeId) {
2982+
return b.createCyclicStructurePlaceholder()
2983+
}
2984+
return b.visitAndTransformType(t, (*NodeBuilderImpl).createTypeNodeFromObjectType)
29722985
} else {
2973-
// Anonymous types without a symbol are never circular.
2986+
// Reverse mapped types use property and index signature placeholders for display.
29742987
return b.createTypeNodeFromObjectType(t)
29752988
}
29762989
}
@@ -3249,6 +3262,19 @@ func (b *NodeBuilderImpl) visitAndTransformType(t *Type, transform func(b *NodeB
32493262
}
32503263
}
32513264

3265+
if t.objectFlags&ObjectFlagsReverseMapped != 0 {
3266+
// Growing type arguments can prevent a reverse mapped type from repeating.
3267+
// Bound expansion by its mapped declaration as well as its type identity.
3268+
origin := CompositeSymbolIdentity{nodeId: ast.GetNodeId(t.AsReverseMappedType().mappedType.AsMappedType().declaration.AsNode())}
3269+
depth := b.ctx.symbolDepth[origin]
3270+
if depth >= 100 {
3271+
b.ctx.truncating = true
3272+
return b.createElidedInformationPlaceholder()
3273+
}
3274+
b.ctx.symbolDepth[origin] = depth + 1
3275+
defer func() { b.ctx.symbolDepth[origin] = depth }()
3276+
}
3277+
32523278
var depth int
32533279
if id != nil {
32543280
depth = b.ctx.symbolDepth[*id]
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
recursive.ts(10,14): error TS5088: The inferred type of 'recursive' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
2+
recursive.ts(17,14): error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
3+
recursive.ts(23,14): error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
4+
5+
6+
==== finite.ts (0 errors) ====
7+
declare function unwrap<T>(input: { [K in keyof T]: { value: T[K] } }): T;
8+
declare const indexedInput: Record<string, { value: string }>;
9+
export const indexed = unwrap(indexedInput);
10+
export const explicit: Record<string, string> = unwrap(indexedInput);
11+
export const shallow = unwrap({ name: { value: "text" } });
12+
13+
declare const readonlyInput: {
14+
readonly [key: string]: { value: string | number };
15+
};
16+
export const readonlyIndexed = unwrap(readonlyInput);
17+
18+
type Validator<T> = ((input: unknown) => T | undefined) | {
19+
[K in keyof T]: Validator<T[K]>;
20+
};
21+
declare function decode<T>(input: { [K in keyof T]: Validator<T[K]> }): T;
22+
declare const stringValidator: (input: unknown) => string | undefined;
23+
24+
export const deep = decode({ a: { b: { c: { d: stringValidator } } } });
25+
interface NamedInput {
26+
leaf: typeof stringValidator;
27+
}
28+
declare const namedInput: { node: NamedInput };
29+
export const named = decode(namedInput);
30+
31+
declare const nestedIndexInput: { node: Record<string, typeof stringValidator> };
32+
export const nestedIndexed = decode(nestedIndexInput);
33+
34+
export const namedExplicit: { node: { leaf: string } } = decode(namedInput);
35+
export const deepExplicit: { a: { b: { c: { d: string } } } } =
36+
decode({ a: { b: { c: { d: stringValidator } } } });
37+
38+
==== recursive.ts (3 errors) ====
39+
type Validator<T> = ((input: unknown) => T | undefined) | {
40+
[K in keyof T]: Validator<T[K]>;
41+
};
42+
declare function decode<T>(input: { [K in keyof T]: Validator<T[K]> }): T;
43+
type RecursiveInput = {
44+
value: (input: unknown) => string;
45+
next: RecursiveInput;
46+
};
47+
declare const recursiveInput: RecursiveInput;
48+
export const recursive = decode(recursiveInput);
49+
~~~~~~~~~
50+
!!! error TS5088: The inferred type of 'recursive' references a type with a cyclic structure which cannot be trivially serialized. A type annotation is necessary.
51+
52+
interface GrowingInput<T> {
53+
value: (input: unknown) => T;
54+
next: GrowingInput<T[]>;
55+
}
56+
declare const growingInput: GrowingInput<string>;
57+
export const growing = decode(growingInput);
58+
~~~~~~~
59+
!!! error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
60+
61+
interface RecursiveIndexInput {
62+
[key: string]: RecursiveIndexInput;
63+
}
64+
declare const recursiveIndexInput: RecursiveIndexInput;
65+
export const recursiveIndexed = decode(recursiveIndexInput);
66+
~~~~~~~~~~~~~~~~
67+
!!! error TS7056: The inferred type of this node exceeds the maximum length the compiler will serialize. An explicit type annotation is needed.
68+

‎tsc/testdata/baselines/reference/compiler/reverseMappedDeclarationSerialization.js‎

Lines changed: 5 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -76,32 +76,32 @@ export const recursiveIndexed = decode(recursiveIndexInput);
7676

7777
//// [finite.d.ts]
7878
export declare const indexed: {
79-
[x: string]: /*elided*/ any;
79+
[x: string]: string;
8080
};
8181
export declare const explicit: Record<string, string>;
8282
export declare const shallow: {
8383
name: string;
8484
};
8585
export declare const readonlyIndexed: {
86-
readonly [x: string]: /*elided*/ any;
86+
readonly [x: string]: string | number;
8787
};
8888
export declare const deep: {
8989
a: {
9090
b: {
9191
c: {
92-
d: /*elided*/ any;
92+
d: string;
9393
};
9494
};
9595
};
9696
};
9797
export declare const named: {
9898
node: {
99-
leaf: /*elided*/ any;
99+
leaf: string;
100100
};
101101
};
102102
export declare const nestedIndexed: {
103103
node: {
104-
[x: string]: /*elided*/ any;
104+
[x: string]: string;
105105
};
106106
};
107107
export declare const namedExplicit: {
@@ -118,27 +118,3 @@ export declare const deepExplicit: {
118118
};
119119
};
120120
};
121-
//// [recursive.d.ts]
122-
export declare const recursive: {
123-
value: string;
124-
next: {
125-
value: string;
126-
next: {
127-
value: string;
128-
next: {
129-
value: /*elided*/ any;
130-
next: /*elided*/ any;
131-
};
132-
};
133-
};
134-
};
135-
export declare const growing: {
136-
value: string;
137-
next: {
138-
value: /*elided*/ any;
139-
next: /*elided*/ any;
140-
};
141-
};
142-
export declare const recursiveIndexed: {
143-
[x: string]: /*elided*/ any;
144-
};

0 commit comments

Comments
 (0)