Skip to content

Commit 5a1261b

Browse files
Copilotjakebailey
andauthored
Preserve parsed typeof X in postfix type contexts (#3964)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: jakebailey <5341706+jakebailey@users.noreply.github.com>
1 parent 1bf0f86 commit 5a1261b

12 files changed

Lines changed: 190 additions & 42 deletions

internal/printer/printer.go

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1962,12 +1962,27 @@ func (p *Printer) emitTypeLiteral(node *ast.TypeLiteralNode) {
19621962

19631963
func (p *Printer) emitArrayType(node *ast.ArrayTypeNode) {
19641964
state := p.enterNode(node.AsNode())
1965-
p.emitTypeNode(node.ElementType, ast.TypePrecedencePostfix)
1965+
p.emitPostfixTypeOperand(node.ElementType, node.AsNode())
19661966
p.writePunctuation("[")
19671967
p.writePunctuation("]")
19681968
p.exitNode(node.AsNode(), state)
19691969
}
19701970

1971+
// emitPostfixTypeOperand emits the operand of a postfix type (ArrayType, IndexedAccessType,
1972+
// OptionalType). It is equivalent to `emitTypeNode(operand, TypePrecedencePostfix)` except
1973+
// that it preserves a parsed `typeof X` operand without adding parentheses (e.g.,
1974+
// `typeof C[K]` instead of `(typeof C)[K]`). TypeScript's `parenthesizeNonArrayTypeOfPostfixType`
1975+
// factory rule wraps `TypeQuery` in `ParenthesizedType` only when a postfix type is constructed
1976+
// via the factory, so parsed postfix types preserve the source as written during round-trip
1977+
// emit while synthesized postfix types (e.g., from declaration emit) still get the parentheses.
1978+
func (p *Printer) emitPostfixTypeOperand(operand *ast.TypeNode, parent *ast.Node) {
1979+
if ast.IsParseTreeNode(parent) && operand.Kind == ast.KindTypeQuery {
1980+
p.emitTypeNode(operand, ast.TypePrecedenceTypeOperator)
1981+
return
1982+
}
1983+
p.emitTypeNode(operand, ast.TypePrecedencePostfix)
1984+
}
1985+
19711986
func (p *Printer) emitTupleElementType(node *ast.Node) {
19721987
p.emitTypeNodeOutsideExtends(node)
19731988
}
@@ -1991,7 +2006,7 @@ func (p *Printer) emitRestType(node *ast.RestTypeNode) {
19912006
func (p *Printer) emitOptionalType(node *ast.OptionalTypeNode) {
19922007
state := p.enterNode(node.AsNode())
19932008
// !!! May need extra parenthesization if we also have JSDocNullableType
1994-
p.emitTypeNode(node.Type, ast.TypePrecedencePostfix)
2009+
p.emitPostfixTypeOperand(node.Type, node.AsNode())
19952010
p.writePunctuation("?")
19962011
p.exitNode(node.AsNode(), state)
19972012
}
@@ -2089,7 +2104,7 @@ func (p *Printer) emitTypeOperator(node *ast.TypeOperatorNode) {
20892104

20902105
func (p *Printer) emitIndexedAccessType(node *ast.IndexedAccessTypeNode) {
20912106
state := p.enterNode(node.AsNode())
2092-
p.emitTypeNode(node.ObjectType, ast.TypePrecedencePostfix)
2107+
p.emitPostfixTypeOperand(node.ObjectType, node.AsNode())
20932108
p.writePunctuation("[")
20942109
p.emitTypeNodeOutsideExtends(node.IndexType)
20952110
p.writePunctuation("]")
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//// [tests/cases/compiler/declarationEmitTypeofIndexedAccessNoParens.ts] ////
2+
3+
//// [declarationEmitTypeofIndexedAccessNoParens.ts]
4+
export const C = { A: 1 };
5+
export type C = typeof C[keyof typeof C];
6+
7+
// Parenthesized form should also round-trip
8+
export type C2 = (typeof C)[keyof typeof C];
9+
10+
// IndexedAccessType: index access of a parsed typeof should preserve source
11+
export const arr = [C];
12+
export type ArrAlias = typeof arr[number];
13+
14+
// ArrayType: array of a parsed typeof should preserve source
15+
export type CArr = typeof C[];
16+
// Parenthesized array form should also round-trip
17+
export type CArr2 = (typeof C)[];
18+
19+
// OptionalType (tuple element): optional of a parsed typeof should preserve source
20+
export type CTuple = [typeof C?];
21+
// Parenthesized optional form should also round-trip
22+
export type CTuple2 = [(typeof C)?];
23+
24+
25+
26+
27+
28+
//// [declarationEmitTypeofIndexedAccessNoParens.d.ts]
29+
export declare const C: {
30+
A: number;
31+
};
32+
export type C = typeof C[keyof typeof C];
33+
export type C2 = (typeof C)[keyof typeof C];
34+
export declare const arr: {
35+
A: number;
36+
}[];
37+
export type ArrAlias = typeof arr[number];
38+
export type CArr = typeof C[];
39+
export type CArr2 = (typeof C)[];
40+
export type CTuple = [typeof C?];
41+
export type CTuple2 = [(typeof C)?];
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
//// [tests/cases/compiler/declarationEmitTypeofIndexedAccessNoParens.ts] ////
2+
3+
=== declarationEmitTypeofIndexedAccessNoParens.ts ===
4+
export const C = { A: 1 };
5+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
6+
>A : Symbol(A, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 18))
7+
8+
export type C = typeof C[keyof typeof C];
9+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
10+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
11+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
12+
13+
// Parenthesized form should also round-trip
14+
export type C2 = (typeof C)[keyof typeof C];
15+
>C2 : Symbol(C2, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 1, 41))
16+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
17+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
18+
19+
// IndexedAccessType: index access of a parsed typeof should preserve source
20+
export const arr = [C];
21+
>arr : Symbol(arr, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 7, 12))
22+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
23+
24+
export type ArrAlias = typeof arr[number];
25+
>ArrAlias : Symbol(ArrAlias, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 7, 23))
26+
>arr : Symbol(arr, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 7, 12))
27+
28+
// ArrayType: array of a parsed typeof should preserve source
29+
export type CArr = typeof C[];
30+
>CArr : Symbol(CArr, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 8, 42))
31+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
32+
33+
// Parenthesized array form should also round-trip
34+
export type CArr2 = (typeof C)[];
35+
>CArr2 : Symbol(CArr2, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 11, 30))
36+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
37+
38+
// OptionalType (tuple element): optional of a parsed typeof should preserve source
39+
export type CTuple = [typeof C?];
40+
>CTuple : Symbol(CTuple, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 13, 33))
41+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
42+
43+
// Parenthesized optional form should also round-trip
44+
export type CTuple2 = [(typeof C)?];
45+
>CTuple2 : Symbol(CTuple2, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 16, 33))
46+
>C : Symbol(C, Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 12), Decl(declarationEmitTypeofIndexedAccessNoParens.ts, 0, 26))
47+
48+
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//// [tests/cases/compiler/declarationEmitTypeofIndexedAccessNoParens.ts] ////
2+
3+
=== declarationEmitTypeofIndexedAccessNoParens.ts ===
4+
export const C = { A: 1 };
5+
>C : { A: number; }
6+
>{ A: 1 } : { A: number; }
7+
>A : number
8+
>1 : 1
9+
10+
export type C = typeof C[keyof typeof C];
11+
>C : number
12+
>C : { A: number; }
13+
>C : { A: number; }
14+
15+
// Parenthesized form should also round-trip
16+
export type C2 = (typeof C)[keyof typeof C];
17+
>C2 : number
18+
>C : { A: number; }
19+
>C : { A: number; }
20+
21+
// IndexedAccessType: index access of a parsed typeof should preserve source
22+
export const arr = [C];
23+
>arr : { A: number; }[]
24+
>[C] : { A: number; }[]
25+
>C : { A: number; }
26+
27+
export type ArrAlias = typeof arr[number];
28+
>ArrAlias : { A: number; }
29+
>arr : { A: number; }[]
30+
31+
// ArrayType: array of a parsed typeof should preserve source
32+
export type CArr = typeof C[];
33+
>CArr : CArr
34+
>C : { A: number; }
35+
36+
// Parenthesized array form should also round-trip
37+
export type CArr2 = (typeof C)[];
38+
>CArr2 : CArr2
39+
>C : { A: number; }
40+
41+
// OptionalType (tuple element): optional of a parsed typeof should preserve source
42+
export type CTuple = [typeof C?];
43+
>CTuple : CTuple
44+
>C : { A: number; }
45+
46+
// Parenthesized optional form should also round-trip
47+
export type CTuple2 = [(typeof C)?];
48+
>CTuple2 : CTuple2
49+
>C : { A: number; }
50+
51+

testdata/baselines/reference/submodule/compiler/declarationEmitGlobalThisPreserved.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,7 @@ export declare const aObj: {
122122
a4: (isNaN: number) => typeof globalThis.isNaN;
123123
};
124124
export type a4Return = ReturnType<ReturnType<typeof a4>>;
125-
export type a4oReturn = ReturnType<ReturnType<(typeof aObj)['a4']>>;
125+
export type a4oReturn = ReturnType<ReturnType<typeof aObj['a4']>>;
126126
export declare const b1: (isNaN: typeof globalThis.isNaN) => typeof globalThis.isNaN;
127127
export declare const b2: (isNaN: typeof globalThis.isNaN, bar?: typeof globalThis.isNaN) => typeof globalThis.isNaN;
128128
export declare const b3: (isNaN: number, bar: typeof globalThis.isNaN) => typeof globalThis.isNaN;
@@ -134,7 +134,7 @@ export declare const bObj: {
134134
b4: (isNaN: number) => typeof globalThis.isNaN;
135135
};
136136
export type b4Return = ReturnType<ReturnType<typeof b4>>;
137-
export type b4oReturn = ReturnType<ReturnType<(typeof bObj)['b4']>>;
137+
export type b4oReturn = ReturnType<ReturnType<typeof bObj['b4']>>;
138138
export declare function c1(isNaN: typeof globalThis.isNaN): typeof globalThis.isNaN;
139139
export declare function c2(isNaN: typeof globalThis.isNaN, bar?: typeof globalThis.isNaN): typeof globalThis.isNaN;
140140
export declare function c3(isNaN: number, bar: typeof globalThis.isNaN): typeof globalThis.isNaN;
@@ -146,7 +146,7 @@ export declare const cObj: {
146146
c4(isNaN: number): typeof globalThis.isNaN;
147147
};
148148
export type c4Return = ReturnType<ReturnType<typeof c4>>;
149-
export type c4oReturn = ReturnType<ReturnType<(typeof cObj)['c4']>>;
149+
export type c4oReturn = ReturnType<ReturnType<typeof cObj['c4']>>;
150150
export declare function d1(): () => (isNaN: typeof globalThis.isNaN) => typeof globalThis.isNaN;
151151
export declare function d2(): () => (isNaN: typeof globalThis.isNaN, bar?: typeof globalThis.isNaN) => typeof globalThis.isNaN;
152152
export declare function d3(): () => (isNaN: number, bar: typeof globalThis.isNaN) => typeof globalThis.isNaN;

testdata/baselines/reference/submodule/compiler/declarationEmitResolveTypesIfNotReusable.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ declare const a: {
4444
};
4545
export declare const o1: (o: A['a']['b']) => void;
4646
export declare const o2: (o: (typeof a)['a']) => void;
47-
export declare const o3: (o: (typeof a)['a']) => void;
47+
export declare const o3: (o: typeof a['a']) => void;
4848
export declare const o4: (o: keyof (A['a'])) => void;
4949
export {};
5050
//// [main.d.ts]

testdata/baselines/reference/submodule/compiler/declarationEmitResolveTypesIfNotReusable.types

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ export const o2 = (o: (typeof a)['a']) => {}
3434
>a : { readonly a: "value of a"; readonly notNecessary: "X"; }
3535

3636
export const o3 = (o: typeof a['a']) => {}
37-
>o3 : (o: (typeof a)['a']) => void
38-
>(o: typeof a['a']) => {} : (o: (typeof a)['a']) => void
37+
>o3 : (o: typeof a['a']) => void
38+
>(o: typeof a['a']) => {} : (o: typeof a['a']) => void
3939
>o : "value of a"
4040
>a : { readonly a: "value of a"; readonly notNecessary: "X"; }
4141

testdata/baselines/reference/submodule/compiler/declarationEmitResolveTypesIfNotReusable.types.diff

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@
2121
export const o3 = (o: typeof a['a']) => {}
2222
->o3 : (o: (typeof a)["a"]) => void
2323
->(o: typeof a['a']) => {} : (o: (typeof a)["a"]) => void
24-
+>o3 : (o: (typeof a)['a']) => void
25-
+>(o: typeof a['a']) => {} : (o: (typeof a)['a']) => void
24+
+>o3 : (o: typeof a['a']) => void
25+
+>(o: typeof a['a']) => {} : (o: typeof a['a']) => void
2626
>o : "value of a"
2727
>a : { readonly a: "value of a"; readonly notNecessary: "X"; }
2828

testdata/baselines/reference/submoduleAccepted/compiler/declarationEmitGlobalThisPreserved.js.diff

Lines changed: 0 additions & 29 deletions
This file was deleted.

testdata/baselines/reference/submoduleAccepted/compiler/declarationEmitResolveTypesIfNotReusable.js.diff

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
-export declare const o4: (o: keyof A["a"]) => void;
1111
+export declare const o1: (o: A['a']['b']) => void;
1212
+export declare const o2: (o: (typeof a)['a']) => void;
13-
+export declare const o3: (o: (typeof a)['a']) => void;
13+
+export declare const o3: (o: typeof a['a']) => void;
1414
+export declare const o4: (o: keyof (A['a'])) => void;
1515
export {};
1616
//// [main.d.ts]

0 commit comments

Comments
 (0)