ts-codemod-lib 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +33 -0
- package/dist/cmd/convert-to-readonly.d.mts +3 -0
- package/dist/cmd/convert-to-readonly.d.mts.map +1 -0
- package/dist/cmd/convert-to-readonly.mjs +137 -0
- package/dist/cmd/convert-to-readonly.mjs.map +1 -0
- package/dist/entry-point.d.mts +2 -0
- package/dist/entry-point.d.mts.map +1 -0
- package/dist/entry-point.mjs +19 -0
- package/dist/entry-point.mjs.map +1 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts +7 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.d.mts.map +1 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs +83 -0
- package/dist/functions/ast-transformers/convert-interface-to-type.mjs.map +1 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts +29 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.d.mts.map +1 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.mjs +811 -0
- package/dist/functions/ast-transformers/convert-to-readonly-type.mjs.map +1 -0
- package/dist/functions/ast-transformers/index.d.mts +7 -0
- package/dist/functions/ast-transformers/index.d.mts.map +1 -0
- package/dist/functions/ast-transformers/index.mjs +9 -0
- package/dist/functions/ast-transformers/index.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts +3 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs +22 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts +2 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs +15 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/constants.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts +21 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs +72 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts +5 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs +5 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/index.mjs.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts +37 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.d.mts.map +1 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs +19 -0
- package/dist/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mjs.map +1 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts +7 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.d.mts.map +1 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs +173 -0
- package/dist/functions/ast-transformers/replace-record-with-unknown-record.mjs.map +1 -0
- package/dist/functions/ast-transformers/transform-source-code.d.mts +3 -0
- package/dist/functions/ast-transformers/transform-source-code.d.mts.map +1 -0
- package/dist/functions/ast-transformers/transform-source-code.mjs +27 -0
- package/dist/functions/ast-transformers/transform-source-code.mjs.map +1 -0
- package/dist/functions/ast-transformers/types.d.mts +3 -0
- package/dist/functions/ast-transformers/types.d.mts.map +1 -0
- package/dist/functions/ast-transformers/types.mjs +2 -0
- package/dist/functions/ast-transformers/types.mjs.map +1 -0
- package/dist/functions/constants/ignore-comment-text.d.mts +3 -0
- package/dist/functions/constants/ignore-comment-text.d.mts.map +1 -0
- package/dist/functions/constants/ignore-comment-text.mjs +5 -0
- package/dist/functions/constants/ignore-comment-text.mjs.map +1 -0
- package/dist/functions/constants/index.d.mts +2 -0
- package/dist/functions/constants/index.d.mts.map +1 -0
- package/dist/functions/constants/index.mjs +2 -0
- package/dist/functions/constants/index.mjs.map +1 -0
- package/dist/functions/functions/has-disable-next-line-comment.d.mts +10 -0
- package/dist/functions/functions/has-disable-next-line-comment.d.mts.map +1 -0
- package/dist/functions/functions/has-disable-next-line-comment.mjs +47 -0
- package/dist/functions/functions/has-disable-next-line-comment.mjs.map +1 -0
- package/dist/functions/functions/index.d.mts +9 -0
- package/dist/functions/functions/index.d.mts.map +1 -0
- package/dist/functions/functions/index.mjs +9 -0
- package/dist/functions/functions/index.mjs.map +1 -0
- package/dist/functions/functions/is-as-const-node.d.mts +10 -0
- package/dist/functions/functions/is-as-const-node.d.mts.map +1 -0
- package/dist/functions/functions/is-as-const-node.mjs +30 -0
- package/dist/functions/functions/is-as-const-node.mjs.map +1 -0
- package/dist/functions/functions/is-primitive-type-node.d.mts +15 -0
- package/dist/functions/functions/is-primitive-type-node.d.mts.map +1 -0
- package/dist/functions/functions/is-primitive-type-node.mjs +46 -0
- package/dist/functions/functions/is-primitive-type-node.mjs.map +1 -0
- package/dist/functions/functions/is-readonly-node.d.mts +21 -0
- package/dist/functions/functions/is-readonly-node.d.mts.map +1 -0
- package/dist/functions/functions/is-readonly-node.mjs +30 -0
- package/dist/functions/functions/is-readonly-node.mjs.map +1 -0
- package/dist/functions/functions/is-spread-parameter-node.d.mts +4 -0
- package/dist/functions/functions/is-spread-parameter-node.d.mts.map +1 -0
- package/dist/functions/functions/is-spread-parameter-node.mjs +9 -0
- package/dist/functions/functions/is-spread-parameter-node.mjs.map +1 -0
- package/dist/functions/functions/remove-parentheses.d.mts +3 -0
- package/dist/functions/functions/remove-parentheses.d.mts.map +1 -0
- package/dist/functions/functions/remove-parentheses.mjs +9 -0
- package/dist/functions/functions/remove-parentheses.mjs.map +1 -0
- package/dist/functions/functions/unwrap-readonly.d.mts +3 -0
- package/dist/functions/functions/unwrap-readonly.d.mts.map +1 -0
- package/dist/functions/functions/unwrap-readonly.mjs +8 -0
- package/dist/functions/functions/unwrap-readonly.mjs.map +1 -0
- package/dist/functions/functions/wrap-with-parentheses.d.mts +2 -0
- package/dist/functions/functions/wrap-with-parentheses.d.mts.map +1 -0
- package/dist/functions/functions/wrap-with-parentheses.mjs +4 -0
- package/dist/functions/functions/wrap-with-parentheses.mjs.map +1 -0
- package/dist/functions/index.d.mts +5 -0
- package/dist/functions/index.d.mts.map +1 -0
- package/dist/functions/index.mjs +19 -0
- package/dist/functions/index.mjs.map +1 -0
- package/dist/functions/utils/index.d.mts +2 -0
- package/dist/functions/utils/index.d.mts.map +1 -0
- package/dist/functions/utils/index.mjs +2 -0
- package/dist/functions/utils/index.mjs.map +1 -0
- package/dist/functions/utils/replace-with-debug.d.mts +3 -0
- package/dist/functions/utils/replace-with-debug.d.mts.map +1 -0
- package/dist/functions/utils/replace-with-debug.mjs +7 -0
- package/dist/functions/utils/replace-with-debug.mjs.map +1 -0
- package/dist/globals.d.mts +1 -0
- package/dist/index.d.mts +2 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +19 -0
- package/dist/index.mjs.map +1 -0
- package/dist/tsconfig.json +1 -0
- package/dist/types.d.mts +2 -0
- package/package.json +134 -0
- package/src/cmd/convert-to-readonly.mts +195 -0
- package/src/entry-point.mts +1 -0
- package/src/functions/ast-transformers/convert-interface-to-type.mts +119 -0
- package/src/functions/ast-transformers/convert-interface-to-type.test.mts +295 -0
- package/src/functions/ast-transformers/convert-to-readonly-type.mts +1391 -0
- package/src/functions/ast-transformers/convert-to-readonly-type.test.mts +3653 -0
- package/src/functions/ast-transformers/index.mts +6 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/compare-union-types.mts +24 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/constants.mts +12 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/group-union-types.mts +152 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/index.mts +4 -0
- package/src/functions/ast-transformers/readonly-transformer-helpers/readonly-context.mts +65 -0
- package/src/functions/ast-transformers/replace-record-with-unknown-record.mts +238 -0
- package/src/functions/ast-transformers/transform-source-code.mts +38 -0
- package/src/functions/ast-transformers/types.mts +6 -0
- package/src/functions/constants/ignore-comment-text.mts +3 -0
- package/src/functions/constants/index.mts +1 -0
- package/src/functions/functions/has-disable-next-line-comment.mts +56 -0
- package/src/functions/functions/index.mts +8 -0
- package/src/functions/functions/is-as-const-node.mts +47 -0
- package/src/functions/functions/is-primitive-type-node.mts +301 -0
- package/src/functions/functions/is-readonly-node.mts +247 -0
- package/src/functions/functions/is-spread-parameter-node.mts +13 -0
- package/src/functions/functions/remove-parentheses.mts +7 -0
- package/src/functions/functions/unwrap-readonly.mts +7 -0
- package/src/functions/functions/wrap-with-parentheses.mts +2 -0
- package/src/functions/index.mts +4 -0
- package/src/functions/utils/index.mts +1 -0
- package/src/functions/utils/replace-with-debug.mts +10 -0
- package/src/globals.d.mts +1 -0
- package/src/index.mts +1 -0
|
@@ -0,0 +1,3653 @@
|
|
|
1
|
+
/* eslint-disable import-x/namespace */
|
|
2
|
+
/* eslint-disable tree-shakable/import-star */
|
|
3
|
+
/* eslint-disable vitest/expect-expect */
|
|
4
|
+
import dedent from 'dedent';
|
|
5
|
+
import * as parserTypeScript from 'prettier/parser-typescript';
|
|
6
|
+
import * as prettierPluginEstree from 'prettier/plugins/estree';
|
|
7
|
+
import * as prettier from 'prettier/standalone';
|
|
8
|
+
import {
|
|
9
|
+
convertToReadonlyTypeTransformer,
|
|
10
|
+
type ReadonlyTransformerOptions,
|
|
11
|
+
} from './convert-to-readonly-type.mjs';
|
|
12
|
+
import { transformSourceCode } from './transform-source-code.mjs';
|
|
13
|
+
|
|
14
|
+
const testFn = async ({
|
|
15
|
+
source,
|
|
16
|
+
expected,
|
|
17
|
+
debug,
|
|
18
|
+
options,
|
|
19
|
+
isTsx = false,
|
|
20
|
+
}: Readonly<{
|
|
21
|
+
source: string;
|
|
22
|
+
expected: string;
|
|
23
|
+
debug?: boolean;
|
|
24
|
+
options?: ReadonlyTransformerOptions;
|
|
25
|
+
isTsx?: boolean;
|
|
26
|
+
}>): Promise<void> => {
|
|
27
|
+
if (debug !== true) {
|
|
28
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
29
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
30
|
+
|
|
31
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
32
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
33
|
+
|
|
34
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
35
|
+
vi.spyOn(console, 'trace').mockImplementation(() => {});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const transformedCodeRaw = await formatter(
|
|
39
|
+
transformSourceCode(source, isTsx, [
|
|
40
|
+
convertToReadonlyTypeTransformer(options),
|
|
41
|
+
]),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const expectedFormatted = await formatter(expected);
|
|
45
|
+
|
|
46
|
+
expect(transformedCodeRaw).toBe(expectedFormatted);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Normalizes whitespace in a code string, primarily for comparing AST structure outputs.
|
|
51
|
+
* - Preserves newlines immediately following line comments (`//`).
|
|
52
|
+
* - Collapses other sequences of whitespace (including newlines) into a single space.
|
|
53
|
+
* - WARNING: Does NOT handle Automatic Semicolon Insertion (ASI) correctly.
|
|
54
|
+
* Code relying on ASI may break if this function is used for general transformation.
|
|
55
|
+
* - WARNING: Does NOT preserve formatting within block comments or template literals.
|
|
56
|
+
*
|
|
57
|
+
* @param code The input TypeScript code string.
|
|
58
|
+
* @returns The code string with normalized whitespace.
|
|
59
|
+
*/
|
|
60
|
+
const normalizeWhitespaceForComparison = (code: string): string => {
|
|
61
|
+
// Use a placeholder unlikely to appear in the code
|
|
62
|
+
const placeholder = '___LINE_COMMENT_NEWLINE_PLACEHOLDER___';
|
|
63
|
+
|
|
64
|
+
// 1. Protect newlines immediately following line comments
|
|
65
|
+
const protectedCode = code.replaceAll(/(\/\/.*?)\r?\n/gu, `$1${placeholder}`);
|
|
66
|
+
|
|
67
|
+
// 2. Collapse multiple whitespace characters (including unprotected newlines) into a single space
|
|
68
|
+
const collapsedCode = protectedCode.replaceAll(/\s+/gu, ' ');
|
|
69
|
+
|
|
70
|
+
// 3. Restore the newlines after line comments
|
|
71
|
+
const finalCode = collapsedCode.replaceAll(placeholder, '\n');
|
|
72
|
+
|
|
73
|
+
// 4. Trim leading/trailing whitespace
|
|
74
|
+
return finalCode.trim();
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const formatter = async (code: string): Promise<string> => {
|
|
78
|
+
const formatOnce = await prettier.format(code, {
|
|
79
|
+
parser: 'typescript',
|
|
80
|
+
plugins: [parserTypeScript, prettierPluginEstree],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const whitespaceNormalized = normalizeWhitespaceForComparison(formatOnce);
|
|
84
|
+
|
|
85
|
+
return prettier.format(whitespaceNormalized, {
|
|
86
|
+
parser: 'typescript',
|
|
87
|
+
plugins: [parserTypeScript, prettierPluginEstree],
|
|
88
|
+
});
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
describe(convertToReadonlyTypeTransformer, () => {
|
|
92
|
+
describe('TypeReferences', () => {
|
|
93
|
+
describe('Sets', () => {
|
|
94
|
+
test.each([
|
|
95
|
+
{
|
|
96
|
+
name: 'Type alias',
|
|
97
|
+
source: dedent`
|
|
98
|
+
type Foo = Set<string>
|
|
99
|
+
`,
|
|
100
|
+
expected: dedent`
|
|
101
|
+
type Foo = ReadonlySet<string>
|
|
102
|
+
`,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: 'Variable declaration with type annotation',
|
|
106
|
+
source: dedent`
|
|
107
|
+
const foo: Set<string> = new Set()
|
|
108
|
+
`,
|
|
109
|
+
expected: dedent`
|
|
110
|
+
const foo: ReadonlySet<string> = new Set()
|
|
111
|
+
`,
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
name: 'Variable declaration without type annotation',
|
|
115
|
+
source: dedent`
|
|
116
|
+
const foo = new Set<string>()
|
|
117
|
+
`,
|
|
118
|
+
expected: dedent`
|
|
119
|
+
const foo = new Set<string>()
|
|
120
|
+
`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: 'Type assertion with as',
|
|
124
|
+
source: dedent`
|
|
125
|
+
const foo = new Set() as Set<string>
|
|
126
|
+
`,
|
|
127
|
+
expected: dedent`
|
|
128
|
+
const foo = new Set() as ReadonlySet<string>
|
|
129
|
+
`,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
name: 'Satisfies operator',
|
|
133
|
+
source: dedent`
|
|
134
|
+
const foo = new Set() satisfies Set<string>
|
|
135
|
+
`,
|
|
136
|
+
expected: dedent`
|
|
137
|
+
const foo = new Set() satisfies ReadonlySet<string>
|
|
138
|
+
`,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
name: 'In function args',
|
|
142
|
+
source: dedent`
|
|
143
|
+
function foo(a: Set<number>, b: Promise<Set<number>>) {}
|
|
144
|
+
`,
|
|
145
|
+
expected: dedent`
|
|
146
|
+
function foo(a: ReadonlySet<number>, b: Promise<ReadonlySet<number>>) {}
|
|
147
|
+
`,
|
|
148
|
+
},
|
|
149
|
+
])('$name', testFn);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe('Maps', () => {
|
|
153
|
+
test.each([
|
|
154
|
+
{
|
|
155
|
+
name: 'Type alias',
|
|
156
|
+
source: dedent`
|
|
157
|
+
type Foo = Map<string, string>
|
|
158
|
+
`,
|
|
159
|
+
expected: dedent`
|
|
160
|
+
type Foo = ReadonlyMap<string, string>
|
|
161
|
+
`,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: 'Variable declaration with type annotation',
|
|
165
|
+
source: dedent`
|
|
166
|
+
const foo: Map<string, string> = new Map()
|
|
167
|
+
`,
|
|
168
|
+
expected: dedent`
|
|
169
|
+
const foo: ReadonlyMap<string, string> = new Map()
|
|
170
|
+
`,
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
name: 'Variable declaration without type annotation',
|
|
174
|
+
source: dedent`
|
|
175
|
+
const foo = new Map<string, string>()
|
|
176
|
+
`,
|
|
177
|
+
expected: dedent`
|
|
178
|
+
const foo = new Map<string, string>()
|
|
179
|
+
`,
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
name: 'Type assertion with as',
|
|
183
|
+
source: dedent`
|
|
184
|
+
const foo = new Map() as Map<string, string>
|
|
185
|
+
`,
|
|
186
|
+
expected: dedent`
|
|
187
|
+
const foo = new Map() as ReadonlyMap<string, string>
|
|
188
|
+
`,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
name: 'Satisfies operator',
|
|
192
|
+
source: dedent`
|
|
193
|
+
const foo = new Map() satisfies Map<string, string>
|
|
194
|
+
`,
|
|
195
|
+
expected: dedent`
|
|
196
|
+
const foo = new Map() satisfies ReadonlyMap<string, string>;
|
|
197
|
+
`,
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
name: 'In function args',
|
|
201
|
+
source: dedent`
|
|
202
|
+
function foo(a: Map<string, number>, b: Promise<Map<string, number>>) {}
|
|
203
|
+
`,
|
|
204
|
+
expected: dedent`
|
|
205
|
+
function foo(a: ReadonlyMap<string, number>, b: Promise<ReadonlyMap<string, number>>) {}
|
|
206
|
+
`,
|
|
207
|
+
},
|
|
208
|
+
])('$name', testFn);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe('Normalize `Readonly` wrappers', () => {
|
|
212
|
+
test.each([
|
|
213
|
+
{
|
|
214
|
+
name: 'Array type wrapped with `Readonly`',
|
|
215
|
+
source: dedent`
|
|
216
|
+
type T = Readonly<1[]>
|
|
217
|
+
`,
|
|
218
|
+
expected: dedent`
|
|
219
|
+
type T = readonly 1[];
|
|
220
|
+
`, // Readonly<T[]> -> readonly T[]
|
|
221
|
+
},
|
|
222
|
+
{
|
|
223
|
+
name: 'Readonly array type wrapped with `Readonly`',
|
|
224
|
+
source: dedent`
|
|
225
|
+
type T = Readonly<readonly 2[]>
|
|
226
|
+
`,
|
|
227
|
+
expected: dedent`
|
|
228
|
+
type T = readonly 2[];
|
|
229
|
+
`, // Readonly<readonly T[]> -> readonly T[]
|
|
230
|
+
},
|
|
231
|
+
{
|
|
232
|
+
name: 'Tuple type wrapped with `Readonly`',
|
|
233
|
+
source: dedent`
|
|
234
|
+
type T = Readonly<[1, 2]>
|
|
235
|
+
`,
|
|
236
|
+
expected: dedent`
|
|
237
|
+
type T = readonly [1, 2];
|
|
238
|
+
`, // Readonly<[T]> -> readonly [T]
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
name: 'Readonly tuple type wrapped with `Readonly`',
|
|
242
|
+
source: dedent`
|
|
243
|
+
type T = Readonly<readonly [1, 2, 3]>
|
|
244
|
+
`,
|
|
245
|
+
expected: dedent`
|
|
246
|
+
type T = readonly [1, 2, 3];
|
|
247
|
+
`, // Readonly<readonly [T]> -> readonly [T]
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'Nested Readonly wrapper',
|
|
251
|
+
source: dedent`
|
|
252
|
+
type T = Readonly<Readonly<{ x: 3 }>>
|
|
253
|
+
`,
|
|
254
|
+
expected: dedent`
|
|
255
|
+
type T = Readonly<{ x: 3 }>;
|
|
256
|
+
`, // Readonly<Readonly<T>> -> Readonly<T>
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: 'Deeply nested Readonly wrapper',
|
|
260
|
+
source: dedent`
|
|
261
|
+
type T = Readonly<Readonly<Readonly<{ x: 2 }>>>
|
|
262
|
+
`,
|
|
263
|
+
expected: dedent`
|
|
264
|
+
type T = Readonly<{ x: 2 }>
|
|
265
|
+
`,
|
|
266
|
+
},
|
|
267
|
+
{
|
|
268
|
+
name: 'Deeply nested Readonly wrapper with array/parens',
|
|
269
|
+
source: dedent`
|
|
270
|
+
type T = Readonly<((Readonly<Readonly<(({ x: 4 })[])>>))>
|
|
271
|
+
`,
|
|
272
|
+
// Readonly<( (Readonly<Readonly<({ x: 4 }[])>>) )>
|
|
273
|
+
// -> Readonly<Readonly<Readonly<({ x: 4 }[])>>> (remove ParenthesizedTypeNode)
|
|
274
|
+
// -> Readonly<Readonly<({ x: 4 }[])>> (Readonly<Readonly<T>> -> Readonly<T>)
|
|
275
|
+
// -> Readonly<({ x: 4 }[])> (Readonly<Readonly<T>> -> Readonly<T>)
|
|
276
|
+
// -> readonly { x: 4 }[] (Readonly<T[]> -> readonly T[])
|
|
277
|
+
expected: dedent`
|
|
278
|
+
type T = readonly Readonly<{ x: 4 }>[]
|
|
279
|
+
`,
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: 'Intersection within readonly array',
|
|
283
|
+
source: dedent`
|
|
284
|
+
type T = readonly (Readonly<{ x: 1 }> & Readonly<{ y: 2 }>)[];
|
|
285
|
+
`,
|
|
286
|
+
// readonly (Readonly<A> & Readonly<B>)[]
|
|
287
|
+
// -> readonly Readonly<A & B>[] (Inner Intersection Collapse)
|
|
288
|
+
expected: dedent`
|
|
289
|
+
type T = readonly Readonly<{ x: 1 } & { y: 2 }>[]
|
|
290
|
+
`,
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
name: 'Union within readonly array',
|
|
294
|
+
source: dedent`
|
|
295
|
+
type T = readonly (Readonly<{ x: 1 }> | Readonly<{ y: 2 }>)[];
|
|
296
|
+
`,
|
|
297
|
+
// readonly (Readonly<A> | Readonly<B>)[]
|
|
298
|
+
// -> readonly Readonly<A | B>[] (Inner Union Collapse)
|
|
299
|
+
expected: dedent`
|
|
300
|
+
type T = readonly Readonly<{ x: 1 } | { y: 2 }>[]
|
|
301
|
+
`,
|
|
302
|
+
},
|
|
303
|
+
])('$name', testFn);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('Readonly wrapper edge cases', () => {
|
|
307
|
+
test.each([
|
|
308
|
+
{
|
|
309
|
+
name: 'Readonly wrapper on Promise<T[]>',
|
|
310
|
+
source: dedent`
|
|
311
|
+
type Test1 = Readonly<Promise<string[]>>;
|
|
312
|
+
type Test2 = Readonly<Promise<readonly number[]>>
|
|
313
|
+
`,
|
|
314
|
+
expected: dedent`
|
|
315
|
+
type Test1 = Readonly<Promise<readonly string[]>>;
|
|
316
|
+
type Test2 = Readonly<Promise<readonly number[]>>
|
|
317
|
+
`,
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
name: 'Readonly wrapper on CustomGeneric<T[]>',
|
|
321
|
+
source: dedent`
|
|
322
|
+
type Wrapper<T> = { data: T };
|
|
323
|
+
type Test3 = Readonly<Wrapper<Map<string, number[]>>>; // Map and Array are not targets
|
|
324
|
+
`,
|
|
325
|
+
expected: dedent`
|
|
326
|
+
type Wrapper<T> = Readonly<{ data: T }>;
|
|
327
|
+
type Test3 = Readonly<Wrapper<ReadonlyMap<string, readonly number[]>>>; // Map and Array are not targets
|
|
328
|
+
`,
|
|
329
|
+
},
|
|
330
|
+
{
|
|
331
|
+
name: 'Nested Readonly simplification with Promise',
|
|
332
|
+
source: dedent`
|
|
333
|
+
type Test4 = Readonly<Readonly<Promise<string[]>>>
|
|
334
|
+
`,
|
|
335
|
+
// Readonly<Readonly<T>> -> Readonly<T>
|
|
336
|
+
expected: dedent`
|
|
337
|
+
type Test4 = Readonly<Promise<readonly string[]>>
|
|
338
|
+
`,
|
|
339
|
+
},
|
|
340
|
+
])('$name', testFn);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
describe('DeepReadonly', () => {
|
|
344
|
+
test.each([
|
|
345
|
+
{
|
|
346
|
+
name: 'Basic DeepReadonly usage',
|
|
347
|
+
source: dedent`
|
|
348
|
+
type T = DeepReadonly<{ a: number; b: string[] }>
|
|
349
|
+
`,
|
|
350
|
+
expected: dedent`
|
|
351
|
+
type T = DeepReadonly<{ a: number; b: string[] }>
|
|
352
|
+
`,
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
name: 'DeepReadonly with nested objects',
|
|
356
|
+
source: dedent`
|
|
357
|
+
type T = DeepReadonly<{ a: number; b: { c: string[]; d: number } }>;
|
|
358
|
+
`,
|
|
359
|
+
expected: dedent`
|
|
360
|
+
type T = DeepReadonly<{ a: number; b: { c: string[]; d: number } }>;
|
|
361
|
+
`,
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: 'DeepReadonly with arrays',
|
|
365
|
+
source: dedent`
|
|
366
|
+
type T = DeepReadonly<string[]>
|
|
367
|
+
`,
|
|
368
|
+
expected: dedent`
|
|
369
|
+
type T = readonly string[]
|
|
370
|
+
`,
|
|
371
|
+
},
|
|
372
|
+
{
|
|
373
|
+
name: 'DeepReadonly with nested arrays',
|
|
374
|
+
source: dedent`
|
|
375
|
+
type T = DeepReadonly<string[][]>
|
|
376
|
+
`,
|
|
377
|
+
expected: dedent`
|
|
378
|
+
type T = DeepReadonly<string[][]>
|
|
379
|
+
`,
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
name: 'DeepReadonly with Map',
|
|
383
|
+
source: dedent`
|
|
384
|
+
type T = DeepReadonly<Map<string, number[]>>
|
|
385
|
+
`,
|
|
386
|
+
expected: dedent`
|
|
387
|
+
type T = DeepReadonly<ReadonlyMap<string, number[]>>
|
|
388
|
+
`,
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
name: 'DeepReadonly with Set',
|
|
392
|
+
source: dedent`
|
|
393
|
+
type T = DeepReadonly<Set<string[]>>
|
|
394
|
+
`,
|
|
395
|
+
expected: dedent`
|
|
396
|
+
type T = DeepReadonly<ReadonlySet<string[]>>
|
|
397
|
+
`,
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'DeepReadonly with redundant Readonly wrapper',
|
|
401
|
+
source: dedent`
|
|
402
|
+
type T = DeepReadonly<Readonly<{ a: number; b: string[] }>>
|
|
403
|
+
`,
|
|
404
|
+
expected: dedent`
|
|
405
|
+
type T = DeepReadonly<{ a: number; b: string[] }>
|
|
406
|
+
`,
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
name: 'DeepReadonly with redundant readonly array',
|
|
410
|
+
source: dedent`
|
|
411
|
+
type T = DeepReadonly<readonly string[]>
|
|
412
|
+
`,
|
|
413
|
+
expected: dedent`
|
|
414
|
+
type T = readonly string[]
|
|
415
|
+
`,
|
|
416
|
+
},
|
|
417
|
+
{
|
|
418
|
+
name: 'DeepReadonly with redundant readonly properties',
|
|
419
|
+
source: dedent`
|
|
420
|
+
type T = DeepReadonly<{ readonly a: number; readonly b: readonly string[] }>;
|
|
421
|
+
`,
|
|
422
|
+
expected: dedent`
|
|
423
|
+
type T = DeepReadonly<{ a: number; b: string[] }>
|
|
424
|
+
`,
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
name: 'Nested DeepReadonly',
|
|
428
|
+
source: dedent`
|
|
429
|
+
type T = DeepReadonly<DeepReadonly<{ a: number; b: string[] }>>;
|
|
430
|
+
`,
|
|
431
|
+
expected: dedent`
|
|
432
|
+
type T = DeepReadonly<{ a: number; b: string[] }>
|
|
433
|
+
`,
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
name: 'DeepReadonly with union types',
|
|
437
|
+
source: dedent`
|
|
438
|
+
type T = DeepReadonly<{ a: number } | { b: string[] }>
|
|
439
|
+
`,
|
|
440
|
+
expected: dedent`
|
|
441
|
+
type T = DeepReadonly<{ a: number } | { b: string[] }>;
|
|
442
|
+
`,
|
|
443
|
+
},
|
|
444
|
+
{
|
|
445
|
+
name: 'DeepReadonly with intersection types',
|
|
446
|
+
source: dedent`
|
|
447
|
+
type T = DeepReadonly<{ a: number } & { b: string[] }>
|
|
448
|
+
`,
|
|
449
|
+
expected: dedent`
|
|
450
|
+
type T = DeepReadonly<{ a: number } & { b: string[] }>;
|
|
451
|
+
`,
|
|
452
|
+
},
|
|
453
|
+
{
|
|
454
|
+
name: 'DeepReadonly with complex nested structure',
|
|
455
|
+
source: dedent`
|
|
456
|
+
type T = DeepReadonly<{ a: number; b: { c: string[]; d: Map<string, { e: number[] }> } }>;
|
|
457
|
+
`,
|
|
458
|
+
expected: dedent`
|
|
459
|
+
type T = DeepReadonly<{ a: number; b: { c: string[]; d: ReadonlyMap<string, { e: number[] }> } }>;
|
|
460
|
+
`,
|
|
461
|
+
},
|
|
462
|
+
{
|
|
463
|
+
name: 'DeepReadonly in function parameter',
|
|
464
|
+
source: dedent`
|
|
465
|
+
function process<T>(data: DeepReadonly<{ items: T[] }>): T { return data.items[0]; }
|
|
466
|
+
`,
|
|
467
|
+
expected: dedent`
|
|
468
|
+
function process<T>(data: DeepReadonly<{ items: T[] }>): T { return data.items[0]; }
|
|
469
|
+
`,
|
|
470
|
+
},
|
|
471
|
+
{
|
|
472
|
+
name: 'DeepReadonly in function return type',
|
|
473
|
+
source: dedent`
|
|
474
|
+
function getData(): DeepReadonly<{ config: string[]; data: number[] }> { return { config: [], data: [] }; }
|
|
475
|
+
`,
|
|
476
|
+
expected: dedent`
|
|
477
|
+
function getData(): DeepReadonly<{ config: string[]; data: number[] }> { return { config: [], data: [] }; }
|
|
478
|
+
`,
|
|
479
|
+
},
|
|
480
|
+
])('$name', testFn);
|
|
481
|
+
});
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
describe('Arrays', () => {
|
|
485
|
+
test.each([
|
|
486
|
+
{
|
|
487
|
+
name: 'Mutable array types in type alias (non-generic)',
|
|
488
|
+
source: dedent`
|
|
489
|
+
type Foo = number[];
|
|
490
|
+
`,
|
|
491
|
+
expected: dedent`
|
|
492
|
+
type Foo = readonly number[];
|
|
493
|
+
`,
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'Mutable array types in function args (non-generic)',
|
|
497
|
+
source: dedent`
|
|
498
|
+
function foo(a: number[], b: Promise<number[]>) {}
|
|
499
|
+
`,
|
|
500
|
+
expected: dedent`
|
|
501
|
+
function foo(a: readonly number[], b: Promise<readonly number[]>) {}
|
|
502
|
+
`,
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
name: 'Mutable array types in function args (generic)',
|
|
506
|
+
source: dedent`
|
|
507
|
+
function foo(a: Array<number>, b: Promise<Array<number>>) {}
|
|
508
|
+
`,
|
|
509
|
+
expected: dedent`
|
|
510
|
+
function foo(a: readonly number[], b: Promise<readonly number[]>) {}
|
|
511
|
+
`,
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: 'Mutable array types in interface',
|
|
515
|
+
source: dedent`
|
|
516
|
+
interface SimpleInterface {
|
|
517
|
+
a: number[];
|
|
518
|
+
}
|
|
519
|
+
`,
|
|
520
|
+
expected: dedent`
|
|
521
|
+
interface SimpleInterface {
|
|
522
|
+
readonly a: readonly number[];
|
|
523
|
+
}
|
|
524
|
+
`,
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: 'Mutable nested array types (non-generic)',
|
|
528
|
+
source: dedent`
|
|
529
|
+
function foo(a: number[][], b: Promise<number[][]>) {}
|
|
530
|
+
`,
|
|
531
|
+
expected: dedent`
|
|
532
|
+
function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
|
|
533
|
+
`,
|
|
534
|
+
},
|
|
535
|
+
{
|
|
536
|
+
name: 'Mutable nested array types (generic)',
|
|
537
|
+
source: dedent`
|
|
538
|
+
function foo(a: Array<Array<number>>, b: Promise<Array<Array<number>>>) {}
|
|
539
|
+
`,
|
|
540
|
+
expected: dedent`
|
|
541
|
+
function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
|
|
542
|
+
`,
|
|
543
|
+
},
|
|
544
|
+
{
|
|
545
|
+
name: 'Mutable nested array types (generic & non-generic combined)',
|
|
546
|
+
source: dedent`
|
|
547
|
+
function foo(a: Array<number[]>, b: Promise<Array<number>[]>) {}
|
|
548
|
+
`,
|
|
549
|
+
expected: dedent`
|
|
550
|
+
function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
|
|
551
|
+
`,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
name: 'Mutable nested array types (readonly & non-readonly combined)',
|
|
555
|
+
source: dedent`
|
|
556
|
+
function foo(a: readonly number[][], b: Promise< (readonly number[])[]>) {}
|
|
557
|
+
`,
|
|
558
|
+
expected: dedent`
|
|
559
|
+
function foo(a: readonly (readonly number[])[], b: Promise<readonly (readonly number[])[]>) {}
|
|
560
|
+
`,
|
|
561
|
+
},
|
|
562
|
+
{
|
|
563
|
+
name: 'Mutable arrays nested within objects',
|
|
564
|
+
source: dedent`
|
|
565
|
+
function foo(
|
|
566
|
+
a: { p: number[] }[],
|
|
567
|
+
b: Promise<number[][]>
|
|
568
|
+
) {}
|
|
569
|
+
`,
|
|
570
|
+
expected: dedent`
|
|
571
|
+
function foo(
|
|
572
|
+
a: readonly Readonly<{ p: readonly number[] }>[],
|
|
573
|
+
b: Promise<readonly (readonly number[])[]>
|
|
574
|
+
) {}
|
|
575
|
+
`,
|
|
576
|
+
},
|
|
577
|
+
{
|
|
578
|
+
name: 'No type annotations',
|
|
579
|
+
source: dedent`
|
|
580
|
+
const foo = [1, 2, 3];
|
|
581
|
+
function bar(param = [1, 2, 3]) {}
|
|
582
|
+
`,
|
|
583
|
+
expected: dedent`
|
|
584
|
+
const foo = [1, 2, 3];
|
|
585
|
+
function bar(param = [1, 2, 3]) {}
|
|
586
|
+
`,
|
|
587
|
+
},
|
|
588
|
+
{
|
|
589
|
+
name: 'Local types within function',
|
|
590
|
+
source: dedent`
|
|
591
|
+
function foo() {
|
|
592
|
+
type Foo = ReadonlyArray<string>;
|
|
593
|
+
type Bar = Array<string>;
|
|
594
|
+
}
|
|
595
|
+
`,
|
|
596
|
+
expected: dedent`
|
|
597
|
+
function foo() {
|
|
598
|
+
type Foo = readonly string[];
|
|
599
|
+
type Bar = readonly string[];
|
|
600
|
+
}
|
|
601
|
+
`,
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: 'Mutable variable declarations (generic)',
|
|
605
|
+
source: dedent`
|
|
606
|
+
const foo: Array<string> = []
|
|
607
|
+
`,
|
|
608
|
+
expected: dedent`
|
|
609
|
+
const foo: readonly string[] = []
|
|
610
|
+
`,
|
|
611
|
+
},
|
|
612
|
+
{
|
|
613
|
+
name: 'Mutable variable declarations (non-generic)',
|
|
614
|
+
source: dedent`
|
|
615
|
+
const foo: string[] = []
|
|
616
|
+
`,
|
|
617
|
+
expected: dedent`
|
|
618
|
+
const foo: readonly string[] = []
|
|
619
|
+
`,
|
|
620
|
+
},
|
|
621
|
+
{
|
|
622
|
+
name: 'Readonly variable declarations (generic)',
|
|
623
|
+
source: dedent`
|
|
624
|
+
const foo: ReadonlyArray<string> = []
|
|
625
|
+
`,
|
|
626
|
+
expected: dedent`
|
|
627
|
+
const foo: readonly string[] = []
|
|
628
|
+
`,
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
name: 'Readonly variable declarations (non-generic)',
|
|
632
|
+
source: dedent`
|
|
633
|
+
const foo: readonly string[] = []
|
|
634
|
+
`,
|
|
635
|
+
expected: dedent`
|
|
636
|
+
const foo: readonly string[] = []
|
|
637
|
+
`,
|
|
638
|
+
},
|
|
639
|
+
{
|
|
640
|
+
name: 'No type annotation',
|
|
641
|
+
source: dedent`
|
|
642
|
+
const foo = []
|
|
643
|
+
`,
|
|
644
|
+
expected: dedent`
|
|
645
|
+
const foo = []
|
|
646
|
+
`,
|
|
647
|
+
},
|
|
648
|
+
{
|
|
649
|
+
name: 'No type annotation with as const',
|
|
650
|
+
source: dedent`
|
|
651
|
+
const numbers = [1, 2, 3] as const
|
|
652
|
+
`,
|
|
653
|
+
expected: dedent`
|
|
654
|
+
const numbers = [1, 2, 3] as const
|
|
655
|
+
`,
|
|
656
|
+
},
|
|
657
|
+
])('$name', testFn);
|
|
658
|
+
});
|
|
659
|
+
|
|
660
|
+
describe('Tuples', () => {
|
|
661
|
+
test.each([
|
|
662
|
+
{
|
|
663
|
+
name: 'Mutable tuples in type alias',
|
|
664
|
+
source: dedent`
|
|
665
|
+
type Foo = [string, string]
|
|
666
|
+
`,
|
|
667
|
+
expected: dedent`
|
|
668
|
+
type Foo = readonly [string, string]
|
|
669
|
+
`,
|
|
670
|
+
},
|
|
671
|
+
{
|
|
672
|
+
name: 'Variable declaration with type annotation',
|
|
673
|
+
source: dedent`
|
|
674
|
+
const foo: [string, string] = [foo, bar];
|
|
675
|
+
`,
|
|
676
|
+
expected: dedent`
|
|
677
|
+
const foo: readonly [string, string] = [foo, bar];
|
|
678
|
+
`,
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: 'Variable declaration without type annotation',
|
|
682
|
+
source: dedent`
|
|
683
|
+
const foo = [foo, bar];
|
|
684
|
+
`,
|
|
685
|
+
expected: dedent`
|
|
686
|
+
const foo = [foo, bar];
|
|
687
|
+
`,
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: 'Type assertion with as',
|
|
691
|
+
source: dedent`
|
|
692
|
+
const foo = [foo, bar] as [string, string];
|
|
693
|
+
`,
|
|
694
|
+
expected: dedent`
|
|
695
|
+
const foo = [foo, bar] as readonly [string, string];
|
|
696
|
+
`,
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
name: 'Satisfies operator',
|
|
700
|
+
source: dedent`
|
|
701
|
+
const foo = [foo, bar] satisfies [string, string];
|
|
702
|
+
`,
|
|
703
|
+
expected: dedent`
|
|
704
|
+
const foo = [foo, bar] satisfies readonly [string, string];
|
|
705
|
+
`,
|
|
706
|
+
},
|
|
707
|
+
{
|
|
708
|
+
name: 'Nested mutable tuple',
|
|
709
|
+
source: dedent`
|
|
710
|
+
const foo = (tuple: [number, string, [number[], { x: string }]]) => {
|
|
711
|
+
console.log(tuple);
|
|
712
|
+
}
|
|
713
|
+
`,
|
|
714
|
+
expected: dedent`
|
|
715
|
+
const foo = (tuple: readonly [number, string, readonly [readonly number[], Readonly<{ x: string }>]]) => {
|
|
716
|
+
console.log(tuple);
|
|
717
|
+
}
|
|
718
|
+
`,
|
|
719
|
+
},
|
|
720
|
+
{
|
|
721
|
+
name: 'Already readonly tuple',
|
|
722
|
+
source: dedent`
|
|
723
|
+
const foo = (tuple: readonly [number, string, readonly [number, string]]) => {
|
|
724
|
+
console.log(tuple);
|
|
725
|
+
}
|
|
726
|
+
`,
|
|
727
|
+
expected: dedent`
|
|
728
|
+
const foo = (tuple: readonly [number, string, readonly [number, string]]) => {
|
|
729
|
+
console.log(tuple);
|
|
730
|
+
}
|
|
731
|
+
`,
|
|
732
|
+
},
|
|
733
|
+
{
|
|
734
|
+
name: 'Tuple with optional element',
|
|
735
|
+
source: dedent`
|
|
736
|
+
type OptionalTuple = [string, number?]
|
|
737
|
+
`,
|
|
738
|
+
expected: dedent`
|
|
739
|
+
type OptionalTuple = readonly [string, number?]
|
|
740
|
+
`,
|
|
741
|
+
},
|
|
742
|
+
])('$name', testFn);
|
|
743
|
+
|
|
744
|
+
describe('Rest types', () => {
|
|
745
|
+
test.each([
|
|
746
|
+
{
|
|
747
|
+
name: 'Tuple with rest element',
|
|
748
|
+
source: dedent`
|
|
749
|
+
type RestTuple = [string, ...number[]]
|
|
750
|
+
`,
|
|
751
|
+
expected: dedent`
|
|
752
|
+
type RestTuple = readonly [string, ...number[]];
|
|
753
|
+
`,
|
|
754
|
+
},
|
|
755
|
+
{
|
|
756
|
+
name: 'Tuple with rest element nested',
|
|
757
|
+
source: dedent`
|
|
758
|
+
type RestTuple = [string, ...[boolean, ...number[]]];
|
|
759
|
+
`,
|
|
760
|
+
expected: dedent`
|
|
761
|
+
type RestTuple = readonly [string, ...[boolean, ...number[]]];
|
|
762
|
+
`,
|
|
763
|
+
},
|
|
764
|
+
{
|
|
765
|
+
name: 'Tuple with rest element nested (already readonly)',
|
|
766
|
+
source: dedent`
|
|
767
|
+
type RestTuple = readonly [string, ...readonly [boolean, ...readonly number[]]];
|
|
768
|
+
`,
|
|
769
|
+
expected: dedent`
|
|
770
|
+
type RestTuple = readonly [string, ...[boolean, ...number[]]];
|
|
771
|
+
`,
|
|
772
|
+
},
|
|
773
|
+
{
|
|
774
|
+
name: 'Tuple with optional and rest elements',
|
|
775
|
+
source: dedent`
|
|
776
|
+
type OptionalRestTuple = [string?, ...Map<string, number>[]];
|
|
777
|
+
`,
|
|
778
|
+
expected: dedent`
|
|
779
|
+
type OptionalRestTuple = readonly [string?, ...ReadonlyMap<string, number>[]];
|
|
780
|
+
`,
|
|
781
|
+
},
|
|
782
|
+
])('$name', testFn);
|
|
783
|
+
});
|
|
784
|
+
|
|
785
|
+
describe('Named tuples', () => {
|
|
786
|
+
test.each([
|
|
787
|
+
{
|
|
788
|
+
name: 'Named tuples',
|
|
789
|
+
source: dedent`
|
|
790
|
+
type T = [names: string[], values: number[]];
|
|
791
|
+
`,
|
|
792
|
+
expected: dedent`
|
|
793
|
+
type T = readonly [names: readonly string[], values: readonly number[]];
|
|
794
|
+
`,
|
|
795
|
+
},
|
|
796
|
+
{
|
|
797
|
+
name: 'NamedTupleMember with "mut_" prefix',
|
|
798
|
+
source: dedent`
|
|
799
|
+
type T = [names: string[], mut_values: number[]];
|
|
800
|
+
`,
|
|
801
|
+
expected: dedent`
|
|
802
|
+
type T = readonly [names: readonly string[], mut_values: number[]];
|
|
803
|
+
`,
|
|
804
|
+
},
|
|
805
|
+
])('$name', testFn);
|
|
806
|
+
});
|
|
807
|
+
});
|
|
808
|
+
|
|
809
|
+
describe('TypeOperators', () => {
|
|
810
|
+
test.each([
|
|
811
|
+
{
|
|
812
|
+
name: 'Keyof type operator',
|
|
813
|
+
source: dedent`
|
|
814
|
+
type Foo = keyof { a: number[]; b: string[] };
|
|
815
|
+
`,
|
|
816
|
+
expected: dedent`
|
|
817
|
+
type Foo = keyof Readonly<{ a: readonly number[]; b: readonly string[] }>;
|
|
818
|
+
`,
|
|
819
|
+
},
|
|
820
|
+
{
|
|
821
|
+
name: 'Readonly type operator on nested array',
|
|
822
|
+
source: dedent`
|
|
823
|
+
type Foo = readonly number[][]
|
|
824
|
+
`,
|
|
825
|
+
expected: dedent`
|
|
826
|
+
type Foo = readonly (readonly number[])[]
|
|
827
|
+
`,
|
|
828
|
+
},
|
|
829
|
+
])('$name', testFn);
|
|
830
|
+
});
|
|
831
|
+
|
|
832
|
+
describe('Type literals', () => {
|
|
833
|
+
test.each([
|
|
834
|
+
{
|
|
835
|
+
name: 'Type literal with one readonly member (unchanged)',
|
|
836
|
+
source: dedent`
|
|
837
|
+
type T = { readonly a: number; b: string }
|
|
838
|
+
`,
|
|
839
|
+
expected: dedent`
|
|
840
|
+
type T = Readonly<{ a: number; b: string }>
|
|
841
|
+
`,
|
|
842
|
+
},
|
|
843
|
+
{
|
|
844
|
+
name: 'Type literal with no readonly members',
|
|
845
|
+
source: dedent`
|
|
846
|
+
type T = { a: number; b: string }
|
|
847
|
+
`,
|
|
848
|
+
expected: dedent`
|
|
849
|
+
type T = Readonly<{ a: number; b: string }>
|
|
850
|
+
`,
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
name: 'Type literal with all members readonly (normalized)',
|
|
854
|
+
source: dedent`
|
|
855
|
+
type T = { readonly a: number; readonly b: string }
|
|
856
|
+
`,
|
|
857
|
+
// All members readonly -> Normalized to Readonly<{...}> (inner readonly removed)
|
|
858
|
+
expected: dedent`
|
|
859
|
+
type T = Readonly<{ a: number; b: string }>
|
|
860
|
+
`,
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
name: 'Readonly type literal (canonical form, unchanged)',
|
|
864
|
+
source: dedent`
|
|
865
|
+
type T = Readonly<{ a: number; b: string }>
|
|
866
|
+
`,
|
|
867
|
+
expected: dedent`
|
|
868
|
+
type T = Readonly<{ a: number; b: string }>;
|
|
869
|
+
`, // Unchanged (already canonical form)
|
|
870
|
+
},
|
|
871
|
+
{
|
|
872
|
+
name: 'In function args',
|
|
873
|
+
source: dedent`
|
|
874
|
+
function foo(a: { p: number[], readonly q: boolean[] }) {}
|
|
875
|
+
`,
|
|
876
|
+
expected: dedent`
|
|
877
|
+
function foo(a: Readonly<{ p: readonly number[], q: readonly boolean[] }>) {}
|
|
878
|
+
`,
|
|
879
|
+
},
|
|
880
|
+
{
|
|
881
|
+
name: 'Nested, in function args',
|
|
882
|
+
source: dedent`
|
|
883
|
+
function foo(a: { readonly p: string[], q: bigint[] }[]) {}
|
|
884
|
+
`,
|
|
885
|
+
expected: dedent`
|
|
886
|
+
function foo(a: readonly Readonly<{ p: readonly string[], q: readonly bigint[] }>[]) {}
|
|
887
|
+
`,
|
|
888
|
+
},
|
|
889
|
+
{
|
|
890
|
+
name: 'In type alias',
|
|
891
|
+
source: dedent`
|
|
892
|
+
type TypeAlias = {
|
|
893
|
+
a: number[]
|
|
894
|
+
};
|
|
895
|
+
`,
|
|
896
|
+
expected: dedent`
|
|
897
|
+
type TypeAlias = Readonly<{
|
|
898
|
+
a: readonly number[]
|
|
899
|
+
}>;
|
|
900
|
+
`,
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
name: 'Type literals without readonly modifiers',
|
|
904
|
+
source: dedent`
|
|
905
|
+
let foo: {
|
|
906
|
+
a: number,
|
|
907
|
+
b: ReadonlyArray<string>,
|
|
908
|
+
c: () => string,
|
|
909
|
+
d: { readonly [key: string]: string[] },
|
|
910
|
+
[key: string]: string[],
|
|
911
|
+
readonly e: {
|
|
912
|
+
a: number,
|
|
913
|
+
b: ReadonlyArray<string>,
|
|
914
|
+
c: () => string,
|
|
915
|
+
d: { readonly [key: string]: string[] },
|
|
916
|
+
[key: string]: string[],
|
|
917
|
+
}
|
|
918
|
+
};
|
|
919
|
+
`,
|
|
920
|
+
expected: dedent`
|
|
921
|
+
let foo: Readonly<{
|
|
922
|
+
a: number,
|
|
923
|
+
b: readonly string[],
|
|
924
|
+
c: () => string,
|
|
925
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
926
|
+
[key: string]: readonly string[],
|
|
927
|
+
e: Readonly<{
|
|
928
|
+
a: number,
|
|
929
|
+
b: readonly string[],
|
|
930
|
+
c: () => string,
|
|
931
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
932
|
+
[key: string]: readonly string[],
|
|
933
|
+
}>
|
|
934
|
+
}>;
|
|
935
|
+
`,
|
|
936
|
+
},
|
|
937
|
+
{
|
|
938
|
+
name: 'Type literal elements with a readonly modifier in an array',
|
|
939
|
+
source: dedent`
|
|
940
|
+
type foo = ReadonlyArray<{ readonly type: string, readonly code: string }>;
|
|
941
|
+
`,
|
|
942
|
+
expected: dedent`
|
|
943
|
+
type foo = readonly Readonly<{ type: string, code: string }>[];
|
|
944
|
+
`,
|
|
945
|
+
},
|
|
946
|
+
{
|
|
947
|
+
name: 'Type literals with readonly on members',
|
|
948
|
+
source: dedent`
|
|
949
|
+
let foo: {
|
|
950
|
+
readonly a: number,
|
|
951
|
+
readonly b: ReadonlyArray<string>,
|
|
952
|
+
readonly c: () => string,
|
|
953
|
+
readonly d: { readonly [key: string]: string[] },
|
|
954
|
+
readonly [key: string]: string[]
|
|
955
|
+
};
|
|
956
|
+
`,
|
|
957
|
+
expected: dedent`
|
|
958
|
+
let foo: Readonly<{
|
|
959
|
+
a: number,
|
|
960
|
+
b: readonly string[],
|
|
961
|
+
c: () => string,
|
|
962
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
963
|
+
[key: string]: readonly string[]
|
|
964
|
+
}>;
|
|
965
|
+
`,
|
|
966
|
+
},
|
|
967
|
+
{
|
|
968
|
+
name: 'Empty type literal',
|
|
969
|
+
source: dedent`
|
|
970
|
+
type foo = {};
|
|
971
|
+
type bar = Readonly<{}>;
|
|
972
|
+
`,
|
|
973
|
+
expected: dedent`
|
|
974
|
+
type foo = {};
|
|
975
|
+
type bar = Readonly<{}>;
|
|
976
|
+
`,
|
|
977
|
+
},
|
|
978
|
+
{
|
|
979
|
+
name: 'Empty type literal with ignoreEmptyObjectTypes = false',
|
|
980
|
+
source: dedent`
|
|
981
|
+
type foo = {};
|
|
982
|
+
type bar = Readonly<{}>;
|
|
983
|
+
`,
|
|
984
|
+
expected: dedent`
|
|
985
|
+
type foo = Readonly<{}>;
|
|
986
|
+
type bar = Readonly<{}>;
|
|
987
|
+
`,
|
|
988
|
+
options: {
|
|
989
|
+
ignoreEmptyObjectTypes: false,
|
|
990
|
+
},
|
|
991
|
+
},
|
|
992
|
+
])('$name', testFn);
|
|
993
|
+
});
|
|
994
|
+
|
|
995
|
+
describe('Index signatures', () => {
|
|
996
|
+
test.each([
|
|
997
|
+
{
|
|
998
|
+
name: 'Index signature with array value',
|
|
999
|
+
source: dedent`
|
|
1000
|
+
interface Foo {
|
|
1001
|
+
[key: string]: string[]
|
|
1002
|
+
}
|
|
1003
|
+
`,
|
|
1004
|
+
expected: dedent`
|
|
1005
|
+
interface Foo {
|
|
1006
|
+
readonly [key: string]: readonly string[]
|
|
1007
|
+
}
|
|
1008
|
+
`,
|
|
1009
|
+
},
|
|
1010
|
+
{
|
|
1011
|
+
name: 'Index signature with readonly key modifier',
|
|
1012
|
+
source: dedent`
|
|
1013
|
+
interface Bar {
|
|
1014
|
+
readonly [key: string]: string[]
|
|
1015
|
+
}
|
|
1016
|
+
`,
|
|
1017
|
+
expected: dedent`
|
|
1018
|
+
interface Bar {
|
|
1019
|
+
readonly [key: string]: readonly string[]
|
|
1020
|
+
}
|
|
1021
|
+
`,
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: 'Index signature with readonly key and mutable object value',
|
|
1025
|
+
source: dedent`
|
|
1026
|
+
interface Foo {
|
|
1027
|
+
readonly [key: string]: {
|
|
1028
|
+
a: Array<string>;
|
|
1029
|
+
readonly b: Promise<Array<string>>;
|
|
1030
|
+
};
|
|
1031
|
+
}
|
|
1032
|
+
`,
|
|
1033
|
+
expected: dedent`
|
|
1034
|
+
interface Foo {
|
|
1035
|
+
readonly [key: string]: Readonly<{
|
|
1036
|
+
a: readonly string[];
|
|
1037
|
+
b: Promise<readonly string[]>;
|
|
1038
|
+
}>;
|
|
1039
|
+
}
|
|
1040
|
+
`,
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
name: 'Index signature in type alias',
|
|
1044
|
+
source: dedent`
|
|
1045
|
+
type Foo = {
|
|
1046
|
+
readonly [key: string]: string[]
|
|
1047
|
+
}
|
|
1048
|
+
`,
|
|
1049
|
+
expected: dedent`
|
|
1050
|
+
type Foo = Readonly<{
|
|
1051
|
+
[key: string]: readonly string[]
|
|
1052
|
+
}>
|
|
1053
|
+
`,
|
|
1054
|
+
},
|
|
1055
|
+
{
|
|
1056
|
+
name: 'Nested index signature in interface',
|
|
1057
|
+
source: dedent`
|
|
1058
|
+
interface NestedIndexSignatureInterface {
|
|
1059
|
+
[key: string]: {
|
|
1060
|
+
[subkey: string]: string[]
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
`,
|
|
1064
|
+
expected: dedent`
|
|
1065
|
+
interface NestedIndexSignatureInterface {
|
|
1066
|
+
readonly [key: string]: Readonly<{
|
|
1067
|
+
[subkey: string]: readonly string[]
|
|
1068
|
+
}>
|
|
1069
|
+
}
|
|
1070
|
+
`,
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
name: 'Nested index signature in type alias',
|
|
1074
|
+
source: dedent`
|
|
1075
|
+
type NestedIndexSignatureTypeAlias = {
|
|
1076
|
+
[key: string]: {
|
|
1077
|
+
[subkey: string]: string[]
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
`,
|
|
1081
|
+
expected: dedent`
|
|
1082
|
+
type NestedIndexSignatureTypeAlias = Readonly<{
|
|
1083
|
+
[key: string]: Readonly<{
|
|
1084
|
+
[subkey: string]: readonly string[]
|
|
1085
|
+
}>
|
|
1086
|
+
}>
|
|
1087
|
+
`,
|
|
1088
|
+
},
|
|
1089
|
+
{
|
|
1090
|
+
name: 'Variable declaration with index signature type',
|
|
1091
|
+
source: dedent`
|
|
1092
|
+
let foo: { readonly [key: string]: number }
|
|
1093
|
+
`,
|
|
1094
|
+
expected: dedent`
|
|
1095
|
+
let foo: Readonly<{ [key: string]: number }>
|
|
1096
|
+
`,
|
|
1097
|
+
},
|
|
1098
|
+
])('$name', testFn);
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
describe('Mapped types', () => {
|
|
1102
|
+
test.each([
|
|
1103
|
+
{
|
|
1104
|
+
name: 'Mapped type in type alias',
|
|
1105
|
+
source: dedent`
|
|
1106
|
+
type T = { [key in string]: number[] }
|
|
1107
|
+
`,
|
|
1108
|
+
expected: dedent`
|
|
1109
|
+
type T = Readonly<{ [key in string]: readonly number[] }>
|
|
1110
|
+
`,
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: 'Mapped type in type alias (PlusToken)',
|
|
1114
|
+
source: dedent`
|
|
1115
|
+
type T = {+readonly [key in string]: number[] }
|
|
1116
|
+
`,
|
|
1117
|
+
expected: dedent`
|
|
1118
|
+
type T = Readonly<{ [key in string]: readonly number[] }>
|
|
1119
|
+
`,
|
|
1120
|
+
},
|
|
1121
|
+
{
|
|
1122
|
+
name: 'Mapped type in type alias (MinusToken)',
|
|
1123
|
+
source: dedent`
|
|
1124
|
+
type T = { -readonly [key in string]-?: number[] }
|
|
1125
|
+
`,
|
|
1126
|
+
expected: dedent`
|
|
1127
|
+
type T = Readonly<{ [key in string]-?: readonly number[] }>
|
|
1128
|
+
`,
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
name: 'Mapped type in type alias (ReadonlyKeyword)',
|
|
1132
|
+
source: dedent`
|
|
1133
|
+
type T = { readonly [key in string]?: number[] }
|
|
1134
|
+
`,
|
|
1135
|
+
expected: dedent`
|
|
1136
|
+
type T = Readonly<{ [key in string]?: readonly number[] }>
|
|
1137
|
+
`,
|
|
1138
|
+
},
|
|
1139
|
+
{
|
|
1140
|
+
name: 'Mapped type in function arguments with readonly',
|
|
1141
|
+
source: dedent`
|
|
1142
|
+
const func = (x: { readonly [key in string]: number[] }) => {}
|
|
1143
|
+
`,
|
|
1144
|
+
expected: dedent`
|
|
1145
|
+
const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
|
|
1146
|
+
`,
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
name: 'Mapped type in function arguments without readonly',
|
|
1150
|
+
source: dedent`
|
|
1151
|
+
const func = (x: { [key in string]: number[] }) => {}
|
|
1152
|
+
`,
|
|
1153
|
+
expected: dedent`
|
|
1154
|
+
const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
|
|
1155
|
+
`,
|
|
1156
|
+
},
|
|
1157
|
+
{
|
|
1158
|
+
name: 'Mapped type in function arguments with Readonly<*>',
|
|
1159
|
+
source: dedent`
|
|
1160
|
+
const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
|
|
1161
|
+
`,
|
|
1162
|
+
expected: dedent`
|
|
1163
|
+
const func = (x: Readonly<{ [key in string]: readonly number[] }>) => {}
|
|
1164
|
+
`,
|
|
1165
|
+
},
|
|
1166
|
+
])('$name', testFn);
|
|
1167
|
+
});
|
|
1168
|
+
|
|
1169
|
+
describe('Interfaces', () => {
|
|
1170
|
+
test.each([
|
|
1171
|
+
{
|
|
1172
|
+
name: 'Mutable & readonly properties',
|
|
1173
|
+
source: dedent`
|
|
1174
|
+
interface Foo {
|
|
1175
|
+
bar: Array<string>;
|
|
1176
|
+
readonly baz: Promise<string[]>;
|
|
1177
|
+
}
|
|
1178
|
+
`,
|
|
1179
|
+
expected: dedent`
|
|
1180
|
+
interface Foo {
|
|
1181
|
+
readonly bar: readonly string[];
|
|
1182
|
+
readonly baz: Promise<readonly string[]>;
|
|
1183
|
+
}
|
|
1184
|
+
`,
|
|
1185
|
+
},
|
|
1186
|
+
{
|
|
1187
|
+
name: 'Interface with various readonly members',
|
|
1188
|
+
source: dedent`
|
|
1189
|
+
interface Foo {
|
|
1190
|
+
readonly a: number,
|
|
1191
|
+
readonly b: ReadonlyArray<string>,
|
|
1192
|
+
readonly c: () => string,
|
|
1193
|
+
readonly d: { readonly [key: string]: string[] },
|
|
1194
|
+
readonly [key: string]: string[],
|
|
1195
|
+
}
|
|
1196
|
+
`,
|
|
1197
|
+
expected: dedent`
|
|
1198
|
+
interface Foo {
|
|
1199
|
+
readonly a: number,
|
|
1200
|
+
readonly b: readonly string[],
|
|
1201
|
+
readonly c: () => string,
|
|
1202
|
+
readonly d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1203
|
+
readonly [key: string]: readonly string[],
|
|
1204
|
+
}
|
|
1205
|
+
`,
|
|
1206
|
+
},
|
|
1207
|
+
{
|
|
1208
|
+
name: 'Interface with nested readonly members',
|
|
1209
|
+
source: dedent`
|
|
1210
|
+
interface Foo {
|
|
1211
|
+
readonly a: number,
|
|
1212
|
+
readonly b: ReadonlyArray<string>,
|
|
1213
|
+
readonly c: () => string,
|
|
1214
|
+
readonly d: { readonly [key: string]: readonly string[] },
|
|
1215
|
+
readonly [key: string]: readonly string[],
|
|
1216
|
+
readonly e: {
|
|
1217
|
+
readonly a: number,
|
|
1218
|
+
readonly b: ReadonlyArray<string>,
|
|
1219
|
+
readonly c: () => string,
|
|
1220
|
+
readonly d: { readonly [key: string]: readonly string[] },
|
|
1221
|
+
readonly [key: string]: readonly string[],
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
`,
|
|
1225
|
+
expected: dedent`
|
|
1226
|
+
interface Foo {
|
|
1227
|
+
readonly a: number,
|
|
1228
|
+
readonly b: readonly string[],
|
|
1229
|
+
readonly c: () => string,
|
|
1230
|
+
readonly d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1231
|
+
readonly [key: string]: readonly string[],
|
|
1232
|
+
readonly e: Readonly<{
|
|
1233
|
+
a: number,
|
|
1234
|
+
b: readonly string[],
|
|
1235
|
+
c: () => string,
|
|
1236
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1237
|
+
[key: string]: readonly string[],
|
|
1238
|
+
}>
|
|
1239
|
+
}
|
|
1240
|
+
`,
|
|
1241
|
+
},
|
|
1242
|
+
{
|
|
1243
|
+
name: 'Interface with nested non-readonly members',
|
|
1244
|
+
source: dedent`
|
|
1245
|
+
interface Foo {
|
|
1246
|
+
a: number,
|
|
1247
|
+
b: ReadonlyArray<string>,
|
|
1248
|
+
c: () => string,
|
|
1249
|
+
d: { [key: string]: string[] },
|
|
1250
|
+
[key: string]: string[],
|
|
1251
|
+
e: {
|
|
1252
|
+
a: number,
|
|
1253
|
+
b: ReadonlyArray<string>,
|
|
1254
|
+
c: () => string,
|
|
1255
|
+
d: { [key: string]: string[] },
|
|
1256
|
+
[key: string]: string[],
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
`,
|
|
1260
|
+
expected: dedent`
|
|
1261
|
+
interface Foo {
|
|
1262
|
+
readonly a: number,
|
|
1263
|
+
readonly b: readonly string[],
|
|
1264
|
+
readonly c: () => string,
|
|
1265
|
+
readonly d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1266
|
+
readonly [key: string]: readonly string[],
|
|
1267
|
+
readonly e: Readonly<{
|
|
1268
|
+
a: number,
|
|
1269
|
+
b: readonly string[],
|
|
1270
|
+
c: () => string,
|
|
1271
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1272
|
+
[key: string]: readonly string[],
|
|
1273
|
+
}>
|
|
1274
|
+
}
|
|
1275
|
+
`,
|
|
1276
|
+
},
|
|
1277
|
+
{
|
|
1278
|
+
name: 'Interface with call signatures and method signatures',
|
|
1279
|
+
source: dedent`
|
|
1280
|
+
interface Foo {
|
|
1281
|
+
(): void;
|
|
1282
|
+
foo(): void;
|
|
1283
|
+
}
|
|
1284
|
+
`,
|
|
1285
|
+
expected: dedent`
|
|
1286
|
+
interface Foo {
|
|
1287
|
+
(): void;
|
|
1288
|
+
foo(): void;
|
|
1289
|
+
}
|
|
1290
|
+
`,
|
|
1291
|
+
},
|
|
1292
|
+
{
|
|
1293
|
+
name: 'Interface extends clause',
|
|
1294
|
+
source: dedent`
|
|
1295
|
+
interface Foo extends Box<[X[]]> {}
|
|
1296
|
+
`,
|
|
1297
|
+
expected: dedent`
|
|
1298
|
+
interface Foo extends Box<readonly [readonly X[]]> {}
|
|
1299
|
+
`,
|
|
1300
|
+
},
|
|
1301
|
+
{
|
|
1302
|
+
name: 'Interface multiple extends',
|
|
1303
|
+
source: dedent`
|
|
1304
|
+
interface A { a: string[]; }
|
|
1305
|
+
interface B { b: number[]; }
|
|
1306
|
+
interface C extends A, B { c: boolean[]; }
|
|
1307
|
+
`,
|
|
1308
|
+
expected: dedent`
|
|
1309
|
+
interface A { readonly a: readonly string[]; }
|
|
1310
|
+
interface B { readonly b: readonly number[]; }
|
|
1311
|
+
interface C extends A, B { readonly c: readonly boolean[]; }
|
|
1312
|
+
`,
|
|
1313
|
+
},
|
|
1314
|
+
{
|
|
1315
|
+
name: 'Interface with call/construct signatures',
|
|
1316
|
+
source: dedent`
|
|
1317
|
+
interface CallableConstructable {
|
|
1318
|
+
(arg: number[]): string[];
|
|
1319
|
+
new (arg: Map<string, number>): Set<boolean[]>;
|
|
1320
|
+
prop: string[];
|
|
1321
|
+
}
|
|
1322
|
+
`,
|
|
1323
|
+
expected: dedent`
|
|
1324
|
+
interface CallableConstructable {
|
|
1325
|
+
(arg: readonly number[]): readonly string[];
|
|
1326
|
+
new (arg: ReadonlyMap<string, number>): ReadonlySet<readonly boolean[]>;
|
|
1327
|
+
readonly prop: readonly string[];
|
|
1328
|
+
}
|
|
1329
|
+
`,
|
|
1330
|
+
},
|
|
1331
|
+
|
|
1332
|
+
{
|
|
1333
|
+
name: 'Symbol',
|
|
1334
|
+
source: dedent`
|
|
1335
|
+
interface FileSystemDirectoryHandle {
|
|
1336
|
+
[Symbol.asyncIterator](): FileSystemDirectoryHandleAsyncIterator<[string, FileSystemHandle]>;
|
|
1337
|
+
entries(): FileSystemDirectoryHandleAsyncIterator<[string, FileSystemHandle]>;
|
|
1338
|
+
keys(): FileSystemDirectoryHandleAsyncIterator<string>;
|
|
1339
|
+
values(): FileSystemDirectoryHandleAsyncIterator<FileSystemHandle>;
|
|
1340
|
+
}
|
|
1341
|
+
`,
|
|
1342
|
+
expected: dedent`
|
|
1343
|
+
interface FileSystemDirectoryHandle {
|
|
1344
|
+
[Symbol.asyncIterator](): FileSystemDirectoryHandleAsyncIterator<readonly [string, FileSystemHandle]>;
|
|
1345
|
+
entries(): FileSystemDirectoryHandleAsyncIterator<readonly [string, FileSystemHandle]>;
|
|
1346
|
+
keys(): FileSystemDirectoryHandleAsyncIterator<string>;
|
|
1347
|
+
values(): FileSystemDirectoryHandleAsyncIterator<FileSystemHandle>;
|
|
1348
|
+
}
|
|
1349
|
+
`,
|
|
1350
|
+
},
|
|
1351
|
+
])('$name', testFn);
|
|
1352
|
+
});
|
|
1353
|
+
|
|
1354
|
+
describe('Classes', () => {
|
|
1355
|
+
describe('Property declarations', () => {
|
|
1356
|
+
test.each([
|
|
1357
|
+
{
|
|
1358
|
+
name: 'Basic property types',
|
|
1359
|
+
source: dedent`
|
|
1360
|
+
class Foo {
|
|
1361
|
+
a: number[];
|
|
1362
|
+
b: Array<string>;
|
|
1363
|
+
c: readonly boolean[];
|
|
1364
|
+
d: ReadonlyArray<bigint>;
|
|
1365
|
+
}
|
|
1366
|
+
`,
|
|
1367
|
+
expected: dedent`
|
|
1368
|
+
class Foo {
|
|
1369
|
+
readonly a: readonly number[];
|
|
1370
|
+
readonly b: readonly string[];
|
|
1371
|
+
readonly c: readonly boolean[];
|
|
1372
|
+
readonly d: readonly bigint[];
|
|
1373
|
+
}
|
|
1374
|
+
`,
|
|
1375
|
+
},
|
|
1376
|
+
{
|
|
1377
|
+
name: 'Properties with various modifiers',
|
|
1378
|
+
source: dedent`
|
|
1379
|
+
class Klass {
|
|
1380
|
+
foo: number;
|
|
1381
|
+
private bar: number;
|
|
1382
|
+
static baz: number;
|
|
1383
|
+
protected static qux: number;
|
|
1384
|
+
}
|
|
1385
|
+
`,
|
|
1386
|
+
expected: dedent`
|
|
1387
|
+
class Klass {
|
|
1388
|
+
readonly foo: number;
|
|
1389
|
+
private readonly bar: number;
|
|
1390
|
+
static readonly baz: number;
|
|
1391
|
+
protected static readonly qux: number;
|
|
1392
|
+
}
|
|
1393
|
+
`,
|
|
1394
|
+
},
|
|
1395
|
+
{
|
|
1396
|
+
name: 'Static property',
|
|
1397
|
+
source: dedent`
|
|
1398
|
+
class Foo {
|
|
1399
|
+
static a: number[];
|
|
1400
|
+
}
|
|
1401
|
+
`,
|
|
1402
|
+
expected: dedent`
|
|
1403
|
+
class Foo {
|
|
1404
|
+
static readonly a: readonly number[];
|
|
1405
|
+
}
|
|
1406
|
+
`,
|
|
1407
|
+
},
|
|
1408
|
+
{
|
|
1409
|
+
name: 'Class private identifier field',
|
|
1410
|
+
source: dedent`
|
|
1411
|
+
class HashField {
|
|
1412
|
+
#data: string[] = [];
|
|
1413
|
+
getData() { return this.#data; }
|
|
1414
|
+
}
|
|
1415
|
+
`,
|
|
1416
|
+
expected: dedent`
|
|
1417
|
+
class HashField {
|
|
1418
|
+
readonly #data: readonly string[] = [];
|
|
1419
|
+
getData() { return this.#data; }
|
|
1420
|
+
}
|
|
1421
|
+
`,
|
|
1422
|
+
},
|
|
1423
|
+
])('$name', testFn);
|
|
1424
|
+
});
|
|
1425
|
+
|
|
1426
|
+
describe('Methods', () => {
|
|
1427
|
+
test.each([
|
|
1428
|
+
{
|
|
1429
|
+
name: 'Mutable arrays in class method body',
|
|
1430
|
+
source: dedent`
|
|
1431
|
+
class Foo {
|
|
1432
|
+
a() {
|
|
1433
|
+
const b: number[] = [];
|
|
1434
|
+
console.log(b);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
`,
|
|
1438
|
+
expected: dedent`
|
|
1439
|
+
class Foo {
|
|
1440
|
+
a() {
|
|
1441
|
+
const b: readonly number[] = [];
|
|
1442
|
+
console.log(b);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
`,
|
|
1446
|
+
},
|
|
1447
|
+
{
|
|
1448
|
+
name: 'Mutable arrays in class method parameters',
|
|
1449
|
+
source: dedent`
|
|
1450
|
+
class Foo {
|
|
1451
|
+
a(s: string[], p: { x: number }) {}
|
|
1452
|
+
}
|
|
1453
|
+
`,
|
|
1454
|
+
expected: dedent`
|
|
1455
|
+
class Foo {
|
|
1456
|
+
a(s: readonly string[], p: Readonly<{ x: number }>) {}
|
|
1457
|
+
}
|
|
1458
|
+
`,
|
|
1459
|
+
},
|
|
1460
|
+
{
|
|
1461
|
+
name: 'Getter and Setter types',
|
|
1462
|
+
source: dedent`
|
|
1463
|
+
class Foo {
|
|
1464
|
+
get a(): number[] { return []; }
|
|
1465
|
+
set a(s: string[], p: { x: number }) {}
|
|
1466
|
+
}
|
|
1467
|
+
`,
|
|
1468
|
+
expected: dedent`
|
|
1469
|
+
class Foo {
|
|
1470
|
+
get a(): readonly number[] { return []; }
|
|
1471
|
+
set a(s: readonly string[], p: Readonly<{ x: number }>) {}
|
|
1472
|
+
}
|
|
1473
|
+
`,
|
|
1474
|
+
},
|
|
1475
|
+
{
|
|
1476
|
+
name: 'Class static method return/params',
|
|
1477
|
+
source: dedent`
|
|
1478
|
+
class Util {
|
|
1479
|
+
static process(data: Map<string, number[]>): Set<string>[] { return []; }
|
|
1480
|
+
}
|
|
1481
|
+
`,
|
|
1482
|
+
expected: dedent`
|
|
1483
|
+
class Util {
|
|
1484
|
+
static process(data: ReadonlyMap<string, readonly number[]>): readonly ReadonlySet<string>[] { return []; }
|
|
1485
|
+
}
|
|
1486
|
+
`,
|
|
1487
|
+
},
|
|
1488
|
+
])('$name', testFn);
|
|
1489
|
+
});
|
|
1490
|
+
|
|
1491
|
+
describe('Parameter properties', () => {
|
|
1492
|
+
test.each([
|
|
1493
|
+
{
|
|
1494
|
+
name: 'Non-readonly class parameter properties',
|
|
1495
|
+
source: dedent`
|
|
1496
|
+
class Klass {
|
|
1497
|
+
constructor (
|
|
1498
|
+
prop: string,
|
|
1499
|
+
public publicProp: string,
|
|
1500
|
+
protected protectedProp: string,
|
|
1501
|
+
private privateProp: string,
|
|
1502
|
+
) { }
|
|
1503
|
+
}
|
|
1504
|
+
`,
|
|
1505
|
+
expected: dedent`
|
|
1506
|
+
class Klass {
|
|
1507
|
+
constructor (
|
|
1508
|
+
prop: string,
|
|
1509
|
+
public readonly publicProp: string,
|
|
1510
|
+
protected readonly protectedProp: string,
|
|
1511
|
+
private readonly privateProp: string,
|
|
1512
|
+
) { }
|
|
1513
|
+
}
|
|
1514
|
+
`,
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
name: 'Already readonly class parameter properties',
|
|
1518
|
+
source: dedent`
|
|
1519
|
+
class Klass {
|
|
1520
|
+
constructor (
|
|
1521
|
+
readonly prop: string,
|
|
1522
|
+
public readonly publicProp: string,
|
|
1523
|
+
protected readonly protectedProp: string,
|
|
1524
|
+
private readonly privateProp: string,
|
|
1525
|
+
) { }
|
|
1526
|
+
}
|
|
1527
|
+
`,
|
|
1528
|
+
expected: dedent`
|
|
1529
|
+
class Klass {
|
|
1530
|
+
constructor (
|
|
1531
|
+
readonly prop: string,
|
|
1532
|
+
public readonly publicProp: string,
|
|
1533
|
+
protected readonly protectedProp: string,
|
|
1534
|
+
private readonly privateProp: string,
|
|
1535
|
+
) { }
|
|
1536
|
+
}
|
|
1537
|
+
`,
|
|
1538
|
+
},
|
|
1539
|
+
{
|
|
1540
|
+
name: 'Object or array parameter properties',
|
|
1541
|
+
source: dedent`
|
|
1542
|
+
class Klass {
|
|
1543
|
+
constructor (
|
|
1544
|
+
prop: string,
|
|
1545
|
+
public publicProp: Array<string>,
|
|
1546
|
+
protected protectedProp: { a: string },
|
|
1547
|
+
private privateProp: string[],
|
|
1548
|
+
) { }
|
|
1549
|
+
}
|
|
1550
|
+
`,
|
|
1551
|
+
expected: dedent`
|
|
1552
|
+
class Klass {
|
|
1553
|
+
constructor (
|
|
1554
|
+
prop: string,
|
|
1555
|
+
public readonly publicProp: readonly string[],
|
|
1556
|
+
protected readonly protectedProp: Readonly<{ a: string }>,
|
|
1557
|
+
private readonly privateProp: readonly string[],
|
|
1558
|
+
) { }
|
|
1559
|
+
}
|
|
1560
|
+
`,
|
|
1561
|
+
},
|
|
1562
|
+
])('$name', testFn);
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
describe('Index signature', () => {
|
|
1566
|
+
test.each([
|
|
1567
|
+
{
|
|
1568
|
+
name: 'Index signature in class',
|
|
1569
|
+
source: dedent`
|
|
1570
|
+
class Klass {
|
|
1571
|
+
[key: string]: string[]
|
|
1572
|
+
}
|
|
1573
|
+
`,
|
|
1574
|
+
expected: dedent`
|
|
1575
|
+
class Klass {
|
|
1576
|
+
readonly [key: string]: readonly string[]
|
|
1577
|
+
}
|
|
1578
|
+
`,
|
|
1579
|
+
},
|
|
1580
|
+
{
|
|
1581
|
+
name: 'Class with property and index signature',
|
|
1582
|
+
source: dedent`
|
|
1583
|
+
class Klass {
|
|
1584
|
+
x: number[] = [];
|
|
1585
|
+
[key: string]: string[] | number[]
|
|
1586
|
+
}
|
|
1587
|
+
`,
|
|
1588
|
+
expected: dedent`
|
|
1589
|
+
class Klass {
|
|
1590
|
+
readonly x: readonly number[] = [];
|
|
1591
|
+
readonly [key: string]: readonly string[] | readonly number[]
|
|
1592
|
+
}
|
|
1593
|
+
`,
|
|
1594
|
+
},
|
|
1595
|
+
])('$name', testFn);
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
describe('Type Assertions in Initializers', () => {
|
|
1599
|
+
test.each([
|
|
1600
|
+
{
|
|
1601
|
+
name: 'Type Assertions in Property Initializers',
|
|
1602
|
+
source: dedent`
|
|
1603
|
+
class Foo {
|
|
1604
|
+
a: number[] = [] as number[];
|
|
1605
|
+
b: Array<string> = [] as Array<string>;
|
|
1606
|
+
c: readonly boolean[] = [] as readonly boolean[];
|
|
1607
|
+
d: ReadonlyArray<bigint> = [] as ReadonlyArray<bigint>;
|
|
1608
|
+
}
|
|
1609
|
+
`,
|
|
1610
|
+
expected: dedent`
|
|
1611
|
+
class Foo {
|
|
1612
|
+
readonly a: readonly number[] = [] as readonly number[];
|
|
1613
|
+
readonly b: readonly string[] = [] as readonly string[];
|
|
1614
|
+
readonly c: readonly boolean[] = [] as readonly boolean[];
|
|
1615
|
+
readonly d: readonly bigint[] = [] as readonly bigint[];
|
|
1616
|
+
}
|
|
1617
|
+
`,
|
|
1618
|
+
},
|
|
1619
|
+
])('$name', testFn);
|
|
1620
|
+
});
|
|
1621
|
+
});
|
|
1622
|
+
|
|
1623
|
+
describe('Functions', () => {
|
|
1624
|
+
describe('Spread syntax (Rest parameters)', () => {
|
|
1625
|
+
test.each([
|
|
1626
|
+
{
|
|
1627
|
+
name: 'Rest parameter with explicit ReadonlyArray type',
|
|
1628
|
+
source: dedent`
|
|
1629
|
+
function foo(...a: ReadonlyArray<number>) {}
|
|
1630
|
+
`,
|
|
1631
|
+
expected: dedent`
|
|
1632
|
+
function foo(...a: readonly number[]) {}
|
|
1633
|
+
`,
|
|
1634
|
+
},
|
|
1635
|
+
{
|
|
1636
|
+
name: 'Rest parameter with explicit readonly array type',
|
|
1637
|
+
source: dedent`
|
|
1638
|
+
const foo = (...a: readonly number[]) => {}
|
|
1639
|
+
`,
|
|
1640
|
+
expected: dedent`
|
|
1641
|
+
const foo = (...a: readonly number[]) => {}
|
|
1642
|
+
`,
|
|
1643
|
+
},
|
|
1644
|
+
{
|
|
1645
|
+
name: 'Rest parameter with mutable array type',
|
|
1646
|
+
source: dedent`
|
|
1647
|
+
const foo = (...a: unknown[]) => {}
|
|
1648
|
+
`,
|
|
1649
|
+
expected: dedent`
|
|
1650
|
+
const foo = (...a: readonly unknown[]) => {}
|
|
1651
|
+
`,
|
|
1652
|
+
},
|
|
1653
|
+
{
|
|
1654
|
+
name: 'Rest parameter with Readonly',
|
|
1655
|
+
source: dedent`
|
|
1656
|
+
const foo = (...a: Readonly<unknown[]>) => {}
|
|
1657
|
+
`,
|
|
1658
|
+
expected: dedent`
|
|
1659
|
+
const foo = (...a: readonly unknown[]) => {}
|
|
1660
|
+
`,
|
|
1661
|
+
},
|
|
1662
|
+
])('$name', testFn);
|
|
1663
|
+
});
|
|
1664
|
+
|
|
1665
|
+
describe('Return types', () => {
|
|
1666
|
+
test.each([
|
|
1667
|
+
{
|
|
1668
|
+
name: 'Mutable return types (function declaration)',
|
|
1669
|
+
source: dedent`
|
|
1670
|
+
declare function f1(...numbers: ReadonlyArray<number>): Array<number>;
|
|
1671
|
+
declare function f2(...numbers: readonly number[]): number[];
|
|
1672
|
+
declare function f3(...numbers: ReadonlyArray<number>): Promise<Array<number>>;
|
|
1673
|
+
declare function f4(...numbers: number[]): Promise<number[]>;
|
|
1674
|
+
declare function f5(...numbers: Array<number>): Promise<Foo<Array<number>>>;
|
|
1675
|
+
declare function f6(...numbers: Readonly<number[]>): Promise<Foo<number[]>>;
|
|
1676
|
+
`,
|
|
1677
|
+
expected: dedent`
|
|
1678
|
+
declare function f1(...numbers: readonly number[]): readonly number[];
|
|
1679
|
+
declare function f2(...numbers: readonly number[]): readonly number[];
|
|
1680
|
+
declare function f3(...numbers: readonly number[]): Promise<readonly number[]>;
|
|
1681
|
+
declare function f4(...numbers: readonly number[]): Promise<readonly number[]>;
|
|
1682
|
+
declare function f5(...numbers: readonly number[]): Promise<Foo<readonly number[]>>;
|
|
1683
|
+
declare function f6(...numbers: readonly number[]): Promise<Foo<readonly number[]>>;
|
|
1684
|
+
`,
|
|
1685
|
+
},
|
|
1686
|
+
{
|
|
1687
|
+
name: 'Mutable return types (function expression)',
|
|
1688
|
+
source: dedent`
|
|
1689
|
+
const foo = (...numbers: ReadonlyArray<number>): Array<number> => {}
|
|
1690
|
+
const bar = (...numbers: readonly number[]): number[] => {}
|
|
1691
|
+
`,
|
|
1692
|
+
expected: dedent`
|
|
1693
|
+
const foo = (...numbers: readonly number[]): readonly number[] => {}
|
|
1694
|
+
const bar = (...numbers: readonly number[]): readonly number[] => {}
|
|
1695
|
+
`,
|
|
1696
|
+
},
|
|
1697
|
+
{
|
|
1698
|
+
name: 'Mutable return types (class method)',
|
|
1699
|
+
source: dedent`
|
|
1700
|
+
class Foo {
|
|
1701
|
+
foo(...numbers: ReadonlyArray<number>): Array<number> {}
|
|
1702
|
+
}
|
|
1703
|
+
class Bar {
|
|
1704
|
+
foo(...numbers: number[]): number[] {}
|
|
1705
|
+
}
|
|
1706
|
+
`,
|
|
1707
|
+
expected: dedent`
|
|
1708
|
+
class Foo {
|
|
1709
|
+
foo(...numbers: readonly number[]): readonly number[] {}
|
|
1710
|
+
}
|
|
1711
|
+
class Bar {
|
|
1712
|
+
foo(...numbers: readonly number[]): readonly number[] {}
|
|
1713
|
+
}
|
|
1714
|
+
`,
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
name: 'Mutable return types (intersection)',
|
|
1718
|
+
source: dedent`
|
|
1719
|
+
declare function foo(...numbers: ReadonlyArray<number>): { readonly a: Array<number> } & { readonly b: string[] }
|
|
1720
|
+
`,
|
|
1721
|
+
expected: dedent`
|
|
1722
|
+
declare function foo(...numbers: readonly number[]): Readonly<{ a: readonly number[] } & { b: readonly string[] }>
|
|
1723
|
+
`,
|
|
1724
|
+
},
|
|
1725
|
+
{
|
|
1726
|
+
name: 'Mutable return types (union)',
|
|
1727
|
+
source: dedent`
|
|
1728
|
+
declare function foo(...numbers: ReadonlyArray<number>): { readonly a: Array<number> } | { readonly b: string[] }
|
|
1729
|
+
`,
|
|
1730
|
+
expected: dedent`
|
|
1731
|
+
declare function foo(...numbers: readonly number[]): Readonly<{ a: readonly number[] } | { b: readonly string[] }>
|
|
1732
|
+
`,
|
|
1733
|
+
},
|
|
1734
|
+
{
|
|
1735
|
+
name: 'Mutable return types (wrapped in another type)',
|
|
1736
|
+
source: dedent`
|
|
1737
|
+
type Foo<T> = { readonly x: T; };
|
|
1738
|
+
declare function func1(...numbers: ReadonlyArray<number>): Promise<Foo<Array<number>>>
|
|
1739
|
+
declare function func2(...numbers: ReadonlyArray<number>): Promise<Foo<number[]>>
|
|
1740
|
+
`,
|
|
1741
|
+
expected: dedent`
|
|
1742
|
+
type Foo<T> = Readonly<{ x: T; }>;
|
|
1743
|
+
declare function func1(...numbers: readonly number[]): Promise<Foo<readonly number[]>>
|
|
1744
|
+
declare function func2(...numbers: readonly number[]): Promise<Foo<readonly number[]>>
|
|
1745
|
+
`,
|
|
1746
|
+
},
|
|
1747
|
+
{
|
|
1748
|
+
name: 'Mutable return types (conditional)',
|
|
1749
|
+
source: dedent`
|
|
1750
|
+
declare function foo<T>(x: T): T extends Array<number> ? string : number[]
|
|
1751
|
+
`,
|
|
1752
|
+
expected: dedent`
|
|
1753
|
+
declare function foo<T>(x: T): T extends readonly number[] ? string : readonly number[]
|
|
1754
|
+
`,
|
|
1755
|
+
},
|
|
1756
|
+
{
|
|
1757
|
+
name: 'Mutable return type with type assertion',
|
|
1758
|
+
source: dedent`
|
|
1759
|
+
function foo(bar: string): { baz: number } {
|
|
1760
|
+
return {} as { baz: number };
|
|
1761
|
+
}
|
|
1762
|
+
`,
|
|
1763
|
+
expected: dedent`
|
|
1764
|
+
function foo(bar: string): Readonly<{ baz: number }> {
|
|
1765
|
+
return {} as Readonly<{ baz: number }>;
|
|
1766
|
+
}
|
|
1767
|
+
`,
|
|
1768
|
+
},
|
|
1769
|
+
{
|
|
1770
|
+
name: 'Already readonly return type (generic)',
|
|
1771
|
+
source: dedent`
|
|
1772
|
+
function foo(): ReadonlyArray<number> {
|
|
1773
|
+
return [1, 2, 3];
|
|
1774
|
+
}
|
|
1775
|
+
`,
|
|
1776
|
+
expected: dedent`
|
|
1777
|
+
function foo(): readonly number[] {
|
|
1778
|
+
return [1, 2, 3];
|
|
1779
|
+
}
|
|
1780
|
+
`,
|
|
1781
|
+
},
|
|
1782
|
+
{
|
|
1783
|
+
name: 'Already readonly return type (non-generic)',
|
|
1784
|
+
source: dedent`
|
|
1785
|
+
const foo = (): readonly number[] => {
|
|
1786
|
+
return [1, 2, 3];
|
|
1787
|
+
}
|
|
1788
|
+
`,
|
|
1789
|
+
expected: dedent`
|
|
1790
|
+
const foo = (): readonly number[] => {
|
|
1791
|
+
return [1, 2, 3];
|
|
1792
|
+
}
|
|
1793
|
+
`,
|
|
1794
|
+
},
|
|
1795
|
+
{
|
|
1796
|
+
name: 'Implicit readonly return type in function (as const)',
|
|
1797
|
+
source: dedent`
|
|
1798
|
+
function foo() {
|
|
1799
|
+
return [1, 2, 3] as const;
|
|
1800
|
+
}
|
|
1801
|
+
`,
|
|
1802
|
+
expected: dedent`
|
|
1803
|
+
function foo() {
|
|
1804
|
+
return [1, 2, 3] as const;
|
|
1805
|
+
}
|
|
1806
|
+
`,
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: 'Implicit readonly return type in arrow function (as const)',
|
|
1810
|
+
source: dedent`
|
|
1811
|
+
const foo = () => {
|
|
1812
|
+
return [1, 2, 3] as const;
|
|
1813
|
+
};
|
|
1814
|
+
`,
|
|
1815
|
+
expected: dedent`
|
|
1816
|
+
const foo = () => {
|
|
1817
|
+
return [1, 2, 3] as const;
|
|
1818
|
+
};
|
|
1819
|
+
`,
|
|
1820
|
+
},
|
|
1821
|
+
])('$name', testFn);
|
|
1822
|
+
});
|
|
1823
|
+
|
|
1824
|
+
describe('Local variables', () => {
|
|
1825
|
+
test.each([
|
|
1826
|
+
{
|
|
1827
|
+
name: 'Mutable local variables',
|
|
1828
|
+
source: dedent`
|
|
1829
|
+
function foo() {
|
|
1830
|
+
let foo: {
|
|
1831
|
+
a: number,
|
|
1832
|
+
b: ReadonlyArray<string>,
|
|
1833
|
+
c: () => string,
|
|
1834
|
+
d: { [key: string]: string[] },
|
|
1835
|
+
e: { [key: string]: string[] },
|
|
1836
|
+
readonly f: {
|
|
1837
|
+
a: number,
|
|
1838
|
+
b: ReadonlyArray<string>,
|
|
1839
|
+
c: () => string,
|
|
1840
|
+
d: { [key: string]: string[] },
|
|
1841
|
+
[key: string]: string[],
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
};
|
|
1845
|
+
`,
|
|
1846
|
+
expected: dedent`
|
|
1847
|
+
function foo() {
|
|
1848
|
+
let foo: Readonly<{
|
|
1849
|
+
a: number,
|
|
1850
|
+
b: readonly string[],
|
|
1851
|
+
c: () => string,
|
|
1852
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1853
|
+
e: Readonly<{ [key: string]: readonly string[] }>,
|
|
1854
|
+
f: Readonly<{
|
|
1855
|
+
a: number,
|
|
1856
|
+
b: readonly string[],
|
|
1857
|
+
c: () => string,
|
|
1858
|
+
d: Readonly<{ [key: string]: readonly string[] }>,
|
|
1859
|
+
[key: string]: readonly string[],
|
|
1860
|
+
}>
|
|
1861
|
+
}>
|
|
1862
|
+
};
|
|
1863
|
+
`,
|
|
1864
|
+
},
|
|
1865
|
+
])('$name', testFn);
|
|
1866
|
+
});
|
|
1867
|
+
|
|
1868
|
+
describe('Generics', () => {
|
|
1869
|
+
test.each([
|
|
1870
|
+
{
|
|
1871
|
+
name: 'Generic constraint',
|
|
1872
|
+
source: dedent`
|
|
1873
|
+
function process<T extends { data: string[] }>(input: T): T { return input; }
|
|
1874
|
+
`,
|
|
1875
|
+
expected: dedent`
|
|
1876
|
+
function process<T extends Readonly<{ data: readonly string[] }>>(input: T): T { return input; }
|
|
1877
|
+
`,
|
|
1878
|
+
},
|
|
1879
|
+
{
|
|
1880
|
+
name: 'Generic default type',
|
|
1881
|
+
source: dedent`
|
|
1882
|
+
type Container<T = Map<string, number[]>> = { item: T }; type E = Container;
|
|
1883
|
+
`,
|
|
1884
|
+
expected: dedent`
|
|
1885
|
+
type Container<T = ReadonlyMap<string, readonly number[]>> = Readonly<{ item: T }>; type E = Container;
|
|
1886
|
+
`,
|
|
1887
|
+
},
|
|
1888
|
+
{
|
|
1889
|
+
name: 'Generic function using type parameter in array',
|
|
1890
|
+
source: dedent`
|
|
1891
|
+
function wrapArray<T>(input: T): T[] { return [input]; }
|
|
1892
|
+
`,
|
|
1893
|
+
expected: dedent`
|
|
1894
|
+
function wrapArray<T>(input: T): readonly T[] { return [input]; }
|
|
1895
|
+
`,
|
|
1896
|
+
},
|
|
1897
|
+
{
|
|
1898
|
+
name: 'Generic function using type parameter in map/set',
|
|
1899
|
+
source: dedent`
|
|
1900
|
+
function wrapMap<T>(input: T): Map<string, T[]> { return new Map([["key", [input]]]); }
|
|
1901
|
+
`,
|
|
1902
|
+
expected: dedent`
|
|
1903
|
+
function wrapMap<T>(input: T): ReadonlyMap<string, readonly T[]> { return new Map([["key", [input]]]); }
|
|
1904
|
+
`,
|
|
1905
|
+
},
|
|
1906
|
+
{
|
|
1907
|
+
name: 'Generic constraint with conditional type',
|
|
1908
|
+
source: dedent`
|
|
1909
|
+
type Constrained<T extends string[] | number[]> = T extends string[] ? { s: T } : { n: T }; type G = Constrained<boolean[][]>;
|
|
1910
|
+
`,
|
|
1911
|
+
expected: dedent`
|
|
1912
|
+
type Constrained<T extends readonly string[] | readonly number[]> = T extends readonly string[] ? Readonly<{ s: T }> : Readonly<{ n: T }>; type G = Constrained<readonly (readonly boolean[])[]>;
|
|
1913
|
+
`,
|
|
1914
|
+
},
|
|
1915
|
+
])('$name', testFn);
|
|
1916
|
+
});
|
|
1917
|
+
});
|
|
1918
|
+
|
|
1919
|
+
describe('Type predicate', () => {
|
|
1920
|
+
test.each([
|
|
1921
|
+
{
|
|
1922
|
+
name: 'Type predicate for array length',
|
|
1923
|
+
source: dedent`
|
|
1924
|
+
const isArrayOfLength1 = <A,>(
|
|
1925
|
+
array: A[],
|
|
1926
|
+
): array is [A, ...A[]] => array.length >= 1;
|
|
1927
|
+
`,
|
|
1928
|
+
expected: dedent`
|
|
1929
|
+
const isArrayOfLength1 = <A,>(
|
|
1930
|
+
array: readonly A[],
|
|
1931
|
+
): array is readonly [A, ...A[]] => array.length >= 1;
|
|
1932
|
+
`,
|
|
1933
|
+
},
|
|
1934
|
+
])('$name', testFn);
|
|
1935
|
+
});
|
|
1936
|
+
|
|
1937
|
+
describe('Type parameter', () => {
|
|
1938
|
+
test.each([
|
|
1939
|
+
{
|
|
1940
|
+
name: 'Mapped type in complex generic function',
|
|
1941
|
+
source: dedent`
|
|
1942
|
+
const tupleMap = <const T extends unknown[], const B>(
|
|
1943
|
+
tpl: T,
|
|
1944
|
+
mapFn: (a: T[number], index: number) => B,
|
|
1945
|
+
): { [K in keyof T]: B } =>
|
|
1946
|
+
tpl.map(mapFn as (a: unknown, index: number) => B) as {
|
|
1947
|
+
[K in keyof T]: B;
|
|
1948
|
+
};
|
|
1949
|
+
`,
|
|
1950
|
+
expected: dedent`
|
|
1951
|
+
const tupleMap = <const T extends readonly unknown[], const B>(
|
|
1952
|
+
tpl: T,
|
|
1953
|
+
mapFn: (a: T[number], index: number) => B,
|
|
1954
|
+
): Readonly<{ [K in keyof T]: B }> =>
|
|
1955
|
+
tpl.map(mapFn as (a: unknown, index: number) => B) as Readonly<{
|
|
1956
|
+
[K in keyof T]: B;
|
|
1957
|
+
}>;
|
|
1958
|
+
`,
|
|
1959
|
+
},
|
|
1960
|
+
])('$name', testFn);
|
|
1961
|
+
});
|
|
1962
|
+
|
|
1963
|
+
describe('Primitive and keyword types', () => {
|
|
1964
|
+
test.each([
|
|
1965
|
+
{
|
|
1966
|
+
name: 'Basic primitive types',
|
|
1967
|
+
source: dedent`
|
|
1968
|
+
type Primitives = { a: string; b: number; c: boolean; d: bigint; e: symbol; };
|
|
1969
|
+
`,
|
|
1970
|
+
expected: dedent`
|
|
1971
|
+
type Primitives = Readonly<{ a: string; b: number; c: boolean; d: bigint; e: symbol; }>;
|
|
1972
|
+
`,
|
|
1973
|
+
},
|
|
1974
|
+
{
|
|
1975
|
+
name: 'Null, undefined, void',
|
|
1976
|
+
source: dedent`
|
|
1977
|
+
type Special = { a: null; b: undefined; c: void }
|
|
1978
|
+
`,
|
|
1979
|
+
expected: dedent`
|
|
1980
|
+
type Special = Readonly<{ a: null; b: undefined; c: void }>;
|
|
1981
|
+
`,
|
|
1982
|
+
},
|
|
1983
|
+
{
|
|
1984
|
+
name: 'Any, unknown, never',
|
|
1985
|
+
source: dedent`
|
|
1986
|
+
type AnyNever = { a: any; b: unknown; c: never }
|
|
1987
|
+
`,
|
|
1988
|
+
expected: dedent`
|
|
1989
|
+
type AnyNever = Readonly<{ a: any; b: unknown; c: never }>
|
|
1990
|
+
`,
|
|
1991
|
+
},
|
|
1992
|
+
{
|
|
1993
|
+
name: 'Object keyword',
|
|
1994
|
+
source: dedent`
|
|
1995
|
+
type Obj = { a: object }
|
|
1996
|
+
`,
|
|
1997
|
+
expected: dedent`
|
|
1998
|
+
type Obj = Readonly<{ a: object }>
|
|
1999
|
+
`,
|
|
2000
|
+
},
|
|
2001
|
+
{
|
|
2002
|
+
name: 'This type in class context',
|
|
2003
|
+
source: dedent`
|
|
2004
|
+
class MyClass {
|
|
2005
|
+
value: number[];
|
|
2006
|
+
compare(other: this): boolean { return this.value.length === other.value.length; }
|
|
2007
|
+
}
|
|
2008
|
+
`,
|
|
2009
|
+
expected: dedent`
|
|
2010
|
+
class MyClass {
|
|
2011
|
+
readonly value: readonly number[];
|
|
2012
|
+
compare(other: this): boolean { return this.value.length === other.value.length; }
|
|
2013
|
+
}
|
|
2014
|
+
`,
|
|
2015
|
+
},
|
|
2016
|
+
])('$name', testFn);
|
|
2017
|
+
});
|
|
2018
|
+
|
|
2019
|
+
describe('Union and intersection types', () => {
|
|
2020
|
+
test.each([
|
|
2021
|
+
...toUnionAndIntersectionTestCase({
|
|
2022
|
+
title: (op) => `${op} of arrays`,
|
|
2023
|
+
testCase: (op) => ({
|
|
2024
|
+
source: `type Arr = string[] ${op} readonly number[];`,
|
|
2025
|
+
expected: `type Arr = readonly string[] ${op} readonly number[];`,
|
|
2026
|
+
}),
|
|
2027
|
+
}),
|
|
2028
|
+
|
|
2029
|
+
...toUnionAndIntersectionTestCase({
|
|
2030
|
+
title: (op) => `${op} of objects`,
|
|
2031
|
+
testCase: (op) => ({
|
|
2032
|
+
source: `type Obj = { a: string[] } ${op} { b: readonly number[] };`,
|
|
2033
|
+
expected: `type Obj = Readonly<{ a: readonly string[] } ${op} { b: readonly number[] }>;`,
|
|
2034
|
+
}),
|
|
2035
|
+
}),
|
|
2036
|
+
|
|
2037
|
+
...toUnionAndIntersectionTestCase({
|
|
2038
|
+
title: (op) => `${op} including non-object/array`,
|
|
2039
|
+
testCase: (op) => ({
|
|
2040
|
+
source: `type Obj = { a: readonly string[] } ${op} readonly number[];`,
|
|
2041
|
+
expected: `type Obj = Readonly<{ a: readonly string[] }> ${op} readonly number[];`,
|
|
2042
|
+
}),
|
|
2043
|
+
}),
|
|
2044
|
+
|
|
2045
|
+
...toUnionAndIntersectionTestCase({
|
|
2046
|
+
title: (op) => `${op} where only one part is normalized`,
|
|
2047
|
+
testCase: (op) => ({
|
|
2048
|
+
source: `type PartialReadonlyType = Readonly<string[]> ${op} number[];`,
|
|
2049
|
+
expected: `type PartialReadonlyType = readonly string[] ${op} readonly number[];`,
|
|
2050
|
+
}),
|
|
2051
|
+
}),
|
|
2052
|
+
|
|
2053
|
+
...toUnionAndIntersectionTestCase({
|
|
2054
|
+
title: (op) => `${op} of Readonly objects`,
|
|
2055
|
+
testCase: (op) => ({
|
|
2056
|
+
source: `type Obj = Readonly<{ a: string[] }> ${op} Readonly<{ b: readonly number[] }>;`,
|
|
2057
|
+
// Readonly<A> | Readonly<B> -> Readonly<A | B> (Collapse)
|
|
2058
|
+
// Inner types are not normalized (string[], readonly number[])
|
|
2059
|
+
expected: `type Obj = Readonly<{ a: readonly string[] } ${op} { b: readonly number[] }>;`,
|
|
2060
|
+
}),
|
|
2061
|
+
}),
|
|
2062
|
+
|
|
2063
|
+
{
|
|
2064
|
+
name: 'Union of generic and non-generic readonly objects',
|
|
2065
|
+
source: dedent`
|
|
2066
|
+
type IntersectMixed = { readonly a: string[] } & Readonly<{ b: readonly number[] }>;
|
|
2067
|
+
`,
|
|
2068
|
+
expected: dedent`
|
|
2069
|
+
type IntersectMixed = Readonly<{ a: readonly string[] } & { b: readonly number[] }>;
|
|
2070
|
+
`,
|
|
2071
|
+
},
|
|
2072
|
+
{
|
|
2073
|
+
name: 'Intersection of readonly object and non-readonly object',
|
|
2074
|
+
source: dedent`
|
|
2075
|
+
type IntersectMixed = { readonly a: string[] } & { b: readonly number[] };
|
|
2076
|
+
`,
|
|
2077
|
+
expected: dedent`
|
|
2078
|
+
type IntersectMixed = Readonly<{ a: readonly string[] } & { b: readonly number[] }>;
|
|
2079
|
+
`,
|
|
2080
|
+
},
|
|
2081
|
+
{
|
|
2082
|
+
name: 'Nested union/intersection',
|
|
2083
|
+
source: dedent`
|
|
2084
|
+
type Nested = (string[] | { x: Map<string, number[]> }) & { y: Set<boolean[]> };
|
|
2085
|
+
`,
|
|
2086
|
+
// Each part is not a normalization target, so unchanged
|
|
2087
|
+
expected: dedent`
|
|
2088
|
+
type Nested = (readonly string[] | Readonly<{ x: ReadonlyMap<string, readonly number[]> }>)
|
|
2089
|
+
& Readonly<{ y: ReadonlySet<readonly boolean[]> }>;
|
|
2090
|
+
`,
|
|
2091
|
+
},
|
|
2092
|
+
{
|
|
2093
|
+
name: 'Nested union/intersection 2',
|
|
2094
|
+
source: dedent`
|
|
2095
|
+
type Nested = ({ x: number[] } | { y: string[] }) & { z: boolean[] };
|
|
2096
|
+
`,
|
|
2097
|
+
// Each part is not a normalization target, so unchanged
|
|
2098
|
+
expected: dedent`
|
|
2099
|
+
type Nested = Readonly<
|
|
2100
|
+
({ x: readonly number[] } | { y: readonly string[] }) & { z: readonly boolean[] }
|
|
2101
|
+
>;
|
|
2102
|
+
`,
|
|
2103
|
+
},
|
|
2104
|
+
|
|
2105
|
+
...toUnionAndIntersectionTestCase({
|
|
2106
|
+
title: (op) => `${op} where only some become Readonly<*>`,
|
|
2107
|
+
testCase: (op) => ({
|
|
2108
|
+
source: `type PartialReadonly = Readonly<string[]> ${op} number[];`,
|
|
2109
|
+
expected: `type PartialReadonly = readonly string[] ${op} readonly number[];`,
|
|
2110
|
+
}),
|
|
2111
|
+
}),
|
|
2112
|
+
|
|
2113
|
+
...toUnionAndIntersectionTestCase({
|
|
2114
|
+
title: (op) => `${op} collapse with Array types`,
|
|
2115
|
+
testCase: (op) => ({
|
|
2116
|
+
source: `type Arr = Readonly<string[]> ${op} Readonly<number[]>;`,
|
|
2117
|
+
// Readonly<A[]> | Readonly<B[]> -> readonly A[] | readonly B[]
|
|
2118
|
+
// Readonly<A[]> & Readonly<B[]> -> readonly A[] & readonly B[]
|
|
2119
|
+
expected: `type Arr = readonly string[] ${op} readonly number[];`,
|
|
2120
|
+
}),
|
|
2121
|
+
}),
|
|
2122
|
+
|
|
2123
|
+
...toUnionAndIntersectionTestCase({
|
|
2124
|
+
title: (op) => `${op} of readonly arrays in Readonly`,
|
|
2125
|
+
testCase: (op) => ({
|
|
2126
|
+
source: `type Arr = Readonly<readonly string[] ${op} readonly number[]>;`,
|
|
2127
|
+
expected: `type Arr = readonly string[] ${op} readonly number[];`,
|
|
2128
|
+
}),
|
|
2129
|
+
}),
|
|
2130
|
+
|
|
2131
|
+
...toUnionAndIntersectionTestCase({
|
|
2132
|
+
title: (op) => `${op} of primitives`,
|
|
2133
|
+
testCase: (op) => ({
|
|
2134
|
+
source: `type Arr = Readonly<number ${op} boolean ${op} string>;`,
|
|
2135
|
+
expected: `type Arr = number ${op} boolean ${op} string;`,
|
|
2136
|
+
}),
|
|
2137
|
+
}),
|
|
2138
|
+
|
|
2139
|
+
...[
|
|
2140
|
+
(op: '|' | '&') => ({
|
|
2141
|
+
source: `type Mixed = Readonly<Readonly<{ x: string }> ${op} readonly number[]>;`,
|
|
2142
|
+
expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
|
|
2143
|
+
}),
|
|
2144
|
+
(op: '|' | '&') => ({
|
|
2145
|
+
source: `type Mixed = Readonly<{ x: string } ${op} readonly number[]>;`,
|
|
2146
|
+
expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
|
|
2147
|
+
}),
|
|
2148
|
+
(op: '|' | '&') => ({
|
|
2149
|
+
source: `type Mixed = Readonly<{ x: string }> ${op} Readonly<number[]>;`,
|
|
2150
|
+
expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
|
|
2151
|
+
}),
|
|
2152
|
+
(op: '|' | '&') => ({
|
|
2153
|
+
source: `type Mixed = Readonly<{ x: string } ${op} Readonly<number[]>>;`,
|
|
2154
|
+
expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
|
|
2155
|
+
}),
|
|
2156
|
+
(op: '|' | '&') => ({
|
|
2157
|
+
source: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
|
|
2158
|
+
expected: `type Mixed = Readonly<{ x: string }> ${op} readonly number[];`,
|
|
2159
|
+
}),
|
|
2160
|
+
(op: '|' | '&') => ({
|
|
2161
|
+
source: `type Mixed = Readonly<{ x: string } ${op} readonly number[] ${op} { y: number } ${op} readonly string[]>;`,
|
|
2162
|
+
expected: `type Mixed = Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
|
|
2163
|
+
}),
|
|
2164
|
+
(op: '|' | '&') => ({
|
|
2165
|
+
source: `type Mixed = Readonly<{ x: string } ${op} readonly number[] ${op} { y: number } ${op} readonly string[]>;`,
|
|
2166
|
+
expected: `type Mixed = Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
|
|
2167
|
+
}),
|
|
2168
|
+
(op: '|' | '&') => ({
|
|
2169
|
+
source: `type Mixed = Readonly<{ x: string } ${op} number[] ${op} Readonly<{ y: number }> ${op} readonly string[]>;`,
|
|
2170
|
+
expected: `type Mixed = Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
|
|
2171
|
+
}),
|
|
2172
|
+
(op: '|' | '&') => ({
|
|
2173
|
+
source: `type Mixed = Readonly<unknown ${op} { x: string } ${op} number[] ${op} Readonly<{ y: number }> ${op} readonly string[]>;`,
|
|
2174
|
+
expected: `type Mixed = unknown ${op} Readonly<{ x: string } ${op} { y: number }> ${op} readonly number[] ${op} readonly string[];`,
|
|
2175
|
+
}),
|
|
2176
|
+
(op: '|' | '&') => ({
|
|
2177
|
+
source: `type Primitive = Readonly<number ${op} boolean ${op} string>;`,
|
|
2178
|
+
expected: `type Primitive = number ${op} boolean ${op} string;`,
|
|
2179
|
+
}),
|
|
2180
|
+
].flatMap((t, i) =>
|
|
2181
|
+
toUnionAndIntersectionTestCase({
|
|
2182
|
+
title: (op) => `${op} of mixed types ${i}`,
|
|
2183
|
+
testCase: t,
|
|
2184
|
+
}),
|
|
2185
|
+
),
|
|
2186
|
+
|
|
2187
|
+
{
|
|
2188
|
+
name: 'Union with mixed array and object types',
|
|
2189
|
+
source: dedent`
|
|
2190
|
+
type MixedUnion = string[] | { a: number; }
|
|
2191
|
+
`,
|
|
2192
|
+
expected: dedent`
|
|
2193
|
+
type MixedUnion = readonly string[] | Readonly<{ a: number; }>;
|
|
2194
|
+
`,
|
|
2195
|
+
},
|
|
2196
|
+
{
|
|
2197
|
+
name: 'Intersection with mixed array and object types',
|
|
2198
|
+
source: dedent`
|
|
2199
|
+
type MixedIntersection = { id: string; } & { data: number[]; };
|
|
2200
|
+
`,
|
|
2201
|
+
expected: dedent`
|
|
2202
|
+
type MixedIntersection = Readonly<{ id: string; } & { data: readonly number[]; }>;
|
|
2203
|
+
`,
|
|
2204
|
+
},
|
|
2205
|
+
{
|
|
2206
|
+
name: 'Nested unions',
|
|
2207
|
+
source: dedent`
|
|
2208
|
+
type NestedUnion = (string[] | number[]) | { a: boolean[]; }
|
|
2209
|
+
`,
|
|
2210
|
+
expected: dedent`
|
|
2211
|
+
type NestedUnion = (readonly string[] | readonly number[]) | Readonly<{ a: readonly boolean[]; }>;
|
|
2212
|
+
`,
|
|
2213
|
+
},
|
|
2214
|
+
{
|
|
2215
|
+
name: 'Nested intersections',
|
|
2216
|
+
source: dedent`
|
|
2217
|
+
type NestedIntersection = ({ a: string; } & { b: number; }) & { c: boolean[]; };
|
|
2218
|
+
`,
|
|
2219
|
+
expected: dedent`
|
|
2220
|
+
type NestedIntersection = Readonly<({ a: string; } & { b: number; }) & { c: readonly boolean[]; }>;
|
|
2221
|
+
`,
|
|
2222
|
+
},
|
|
2223
|
+
{
|
|
2224
|
+
name: 'Union with generic types',
|
|
2225
|
+
source: dedent`
|
|
2226
|
+
type GenericUnion<T> = T[] | { value: T; }
|
|
2227
|
+
`,
|
|
2228
|
+
expected: dedent`
|
|
2229
|
+
type GenericUnion<T> = readonly T[] | Readonly<{ value: T; }>;
|
|
2230
|
+
`,
|
|
2231
|
+
},
|
|
2232
|
+
{
|
|
2233
|
+
name: 'Intersection with generic types',
|
|
2234
|
+
source: dedent`
|
|
2235
|
+
type GenericIntersection<T> = { id: string; } & { data: T[]; };
|
|
2236
|
+
`,
|
|
2237
|
+
expected: dedent`
|
|
2238
|
+
type GenericIntersection<T> = Readonly<{ id: string; } & { data: readonly T[]; }>;
|
|
2239
|
+
`,
|
|
2240
|
+
},
|
|
2241
|
+
{
|
|
2242
|
+
name: 'Union with DeepReadonly',
|
|
2243
|
+
source: dedent`
|
|
2244
|
+
type DeepReadonlyUnion = DeepReadonly<{ a: number; }> | string[];
|
|
2245
|
+
`,
|
|
2246
|
+
expected: dedent`
|
|
2247
|
+
type DeepReadonlyUnion = DeepReadonly<{ a: number; }> | readonly string[];
|
|
2248
|
+
`,
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
name: 'Complex union with multiple types',
|
|
2252
|
+
source: dedent`
|
|
2253
|
+
type ComplexUnion = string | number[] | { a: boolean; } | Map<string, number[]>;
|
|
2254
|
+
`,
|
|
2255
|
+
expected: dedent`
|
|
2256
|
+
type ComplexUnion = string | readonly number[] | Readonly<{ a: boolean; }> | ReadonlyMap<string, readonly number[]>;
|
|
2257
|
+
`,
|
|
2258
|
+
},
|
|
2259
|
+
{
|
|
2260
|
+
name: 'Union with function types',
|
|
2261
|
+
source: dedent`
|
|
2262
|
+
type FunctionUnion = ((a: string[]) => void) | { handler: (b: number[]) => boolean; };
|
|
2263
|
+
`,
|
|
2264
|
+
expected: dedent`
|
|
2265
|
+
type FunctionUnion = ((a: readonly string[]) => void) | Readonly<{ handler: (b: readonly number[]) => boolean; }>;
|
|
2266
|
+
`,
|
|
2267
|
+
},
|
|
2268
|
+
{
|
|
2269
|
+
// The case where the entire intersection member directly under Readonly has been converted with replaceWithText
|
|
2270
|
+
name: 'Union with Readonly',
|
|
2271
|
+
source: dedent`
|
|
2272
|
+
type ReadonlyUnion = Readonly<{ a: string[]; } | { b: number[]; }>;
|
|
2273
|
+
`,
|
|
2274
|
+
expected: dedent`
|
|
2275
|
+
type ReadonlyUnion = Readonly<{ a: readonly string[]; } | { b: readonly number[]; }>;
|
|
2276
|
+
`,
|
|
2277
|
+
},
|
|
2278
|
+
])('$name', testFn);
|
|
2279
|
+
});
|
|
2280
|
+
|
|
2281
|
+
describe('Parenthesized types', () => {
|
|
2282
|
+
test.each([
|
|
2283
|
+
{
|
|
2284
|
+
name: 'Parenthesized readonly array',
|
|
2285
|
+
source: dedent`
|
|
2286
|
+
type ParenReadonlyArr = (readonly string[])
|
|
2287
|
+
`,
|
|
2288
|
+
expected: dedent`
|
|
2289
|
+
type ParenReadonlyArr = readonly string[]
|
|
2290
|
+
`,
|
|
2291
|
+
},
|
|
2292
|
+
{
|
|
2293
|
+
name: 'Parenthesized nested readonly array',
|
|
2294
|
+
source: dedent`
|
|
2295
|
+
type ParenNestedReadonlyArr = readonly (readonly string[])[]
|
|
2296
|
+
`,
|
|
2297
|
+
expected: dedent`
|
|
2298
|
+
type ParenNestedReadonlyArr = readonly (readonly string[])[];
|
|
2299
|
+
`,
|
|
2300
|
+
},
|
|
2301
|
+
{
|
|
2302
|
+
name: 'Parenthesized readonly tuple',
|
|
2303
|
+
source: dedent`
|
|
2304
|
+
type ParenReadonlyTup = (readonly [string, number])
|
|
2305
|
+
`,
|
|
2306
|
+
expected: dedent`
|
|
2307
|
+
type ParenReadonlyTup = readonly [string, number]
|
|
2308
|
+
`,
|
|
2309
|
+
},
|
|
2310
|
+
{
|
|
2311
|
+
name: 'Parenthesized type literal',
|
|
2312
|
+
source: dedent`
|
|
2313
|
+
type ParenObj = ({ a: number[] })
|
|
2314
|
+
`,
|
|
2315
|
+
expected: dedent`
|
|
2316
|
+
type ParenObj = Readonly<{ a: readonly number[] }>
|
|
2317
|
+
`,
|
|
2318
|
+
},
|
|
2319
|
+
{
|
|
2320
|
+
name: 'Parenthesized type with union/intersection',
|
|
2321
|
+
source: dedent`
|
|
2322
|
+
type Paren = ({ a: string[] } | { b: number[] })[]
|
|
2323
|
+
`,
|
|
2324
|
+
expected: dedent`
|
|
2325
|
+
type Paren = readonly Readonly<{ a: readonly string[] } | { b: readonly number[] }>[]
|
|
2326
|
+
`,
|
|
2327
|
+
},
|
|
2328
|
+
{
|
|
2329
|
+
name: 'Nested Parentheses Removal',
|
|
2330
|
+
source: dedent`
|
|
2331
|
+
type NestedParen = ((readonly number[]))
|
|
2332
|
+
`,
|
|
2333
|
+
// ((readonly T[])) -> (readonly T[]) -> readonly T[]
|
|
2334
|
+
expected: dedent`
|
|
2335
|
+
type NestedParen = readonly number[]
|
|
2336
|
+
`,
|
|
2337
|
+
},
|
|
2338
|
+
{
|
|
2339
|
+
name: 'Parentheses around primitive',
|
|
2340
|
+
source: dedent`
|
|
2341
|
+
type ParenPrim = (number)
|
|
2342
|
+
`,
|
|
2343
|
+
expected: dedent`
|
|
2344
|
+
type ParenPrim = number;
|
|
2345
|
+
`, // Parentheses removed
|
|
2346
|
+
},
|
|
2347
|
+
{
|
|
2348
|
+
name: 'Parentheses around Readonly<T>',
|
|
2349
|
+
source: dedent`
|
|
2350
|
+
type ParenReadonly = (Readonly<{ a: number }>)
|
|
2351
|
+
`,
|
|
2352
|
+
// (Readonly<T>) -> Readonly<T> (Parentheses removed because inner type is TypeReference)
|
|
2353
|
+
expected: dedent`
|
|
2354
|
+
type ParenReadonly = Readonly<{ a: number }>
|
|
2355
|
+
`,
|
|
2356
|
+
},
|
|
2357
|
+
])('$name', testFn);
|
|
2358
|
+
});
|
|
2359
|
+
|
|
2360
|
+
describe('IndexedAccessTypeNode', () => {
|
|
2361
|
+
test.each([
|
|
2362
|
+
{
|
|
2363
|
+
name: 'FlatArray',
|
|
2364
|
+
source: dedent`
|
|
2365
|
+
type FlatArray<Arr, Depth extends number> = {
|
|
2366
|
+
done: Arr;
|
|
2367
|
+
recur: Arr extends ReadonlyArray<infer InnerArr>
|
|
2368
|
+
? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
|
|
2369
|
+
: Arr;
|
|
2370
|
+
}[Depth extends -1 ? 'done' : 'recur'];
|
|
2371
|
+
`,
|
|
2372
|
+
expected: dedent`
|
|
2373
|
+
type FlatArray<Arr, Depth extends number> = {
|
|
2374
|
+
done: Arr;
|
|
2375
|
+
recur: Arr extends readonly (infer InnerArr)[]
|
|
2376
|
+
? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
|
|
2377
|
+
: Arr;
|
|
2378
|
+
}[Depth extends -1 ? 'done' : 'recur'];
|
|
2379
|
+
`,
|
|
2380
|
+
},
|
|
2381
|
+
{
|
|
2382
|
+
name: 'FlatArray (already readonly)',
|
|
2383
|
+
source: dedent`
|
|
2384
|
+
type FlatArray<Arr, Depth extends number> = Readonly<{
|
|
2385
|
+
done: Arr;
|
|
2386
|
+
recur: Arr extends ReadonlyArray<infer InnerArr>
|
|
2387
|
+
? FlatArray<InnerArr, (readonly [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20])[Depth]>
|
|
2388
|
+
: Arr;
|
|
2389
|
+
}>[Depth extends -1 ? 'done' : 'recur'];
|
|
2390
|
+
`,
|
|
2391
|
+
expected: dedent`
|
|
2392
|
+
type FlatArray<Arr, Depth extends number> = {
|
|
2393
|
+
done: Arr;
|
|
2394
|
+
recur: Arr extends readonly (infer InnerArr)[]
|
|
2395
|
+
? FlatArray<InnerArr, [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20][Depth]>
|
|
2396
|
+
: Arr;
|
|
2397
|
+
}[Depth extends -1 ? 'done' : 'recur'];
|
|
2398
|
+
`,
|
|
2399
|
+
},
|
|
2400
|
+
{
|
|
2401
|
+
name: 'Indexed access type deeper',
|
|
2402
|
+
source: dedent`
|
|
2403
|
+
type DeepAccess = {
|
|
2404
|
+
a: { b: { c: number[] } }
|
|
2405
|
+
}["a"]["b"]["c"];
|
|
2406
|
+
`,
|
|
2407
|
+
expected: dedent`
|
|
2408
|
+
type DeepAccess = {
|
|
2409
|
+
a: { b: { c: readonly number[] } }
|
|
2410
|
+
}["a"]["b"]["c"];
|
|
2411
|
+
`,
|
|
2412
|
+
},
|
|
2413
|
+
{
|
|
2414
|
+
name: 'Indexed access type deeper (mixed)',
|
|
2415
|
+
source: dedent`
|
|
2416
|
+
type DeepAccess = {
|
|
2417
|
+
a: [{ b: number[] }, { c: number[] }]
|
|
2418
|
+
}["a"][1]["c"];
|
|
2419
|
+
`,
|
|
2420
|
+
expected: dedent`
|
|
2421
|
+
type DeepAccess = {
|
|
2422
|
+
a: [{ b: readonly number[] }, { c: readonly number[] }]
|
|
2423
|
+
}["a"][1]["c"];
|
|
2424
|
+
`,
|
|
2425
|
+
},
|
|
2426
|
+
{
|
|
2427
|
+
name: 'Indexed access type deeper (mixed, not sequent)',
|
|
2428
|
+
source: dedent`
|
|
2429
|
+
type DeepAccess = {
|
|
2430
|
+
a: {
|
|
2431
|
+
x: [{ b: number[] }, { c: number[] }][1]["c"]
|
|
2432
|
+
}
|
|
2433
|
+
}["a"];
|
|
2434
|
+
`,
|
|
2435
|
+
expected: dedent`
|
|
2436
|
+
type DeepAccess = {
|
|
2437
|
+
a: Readonly<{
|
|
2438
|
+
x: [{ b: readonly number[] }, { c: readonly number[] }][1]["c"]
|
|
2439
|
+
}>
|
|
2440
|
+
}["a"];
|
|
2441
|
+
`,
|
|
2442
|
+
},
|
|
2443
|
+
{
|
|
2444
|
+
name: 'Indexed access type deeper (mixed, not sequent 2)',
|
|
2445
|
+
source: dedent`
|
|
2446
|
+
type DeepAccess = {
|
|
2447
|
+
a: [
|
|
2448
|
+
{ x: string[] },
|
|
2449
|
+
{ y: { c: number[] }["c"] }["y"],
|
|
2450
|
+
]
|
|
2451
|
+
}["a"][1];
|
|
2452
|
+
`,
|
|
2453
|
+
expected: dedent`
|
|
2454
|
+
type DeepAccess = {
|
|
2455
|
+
a: [
|
|
2456
|
+
Readonly<{ x: readonly string[] }>,
|
|
2457
|
+
{ y: { c: readonly number[] }["c"] }["y"],
|
|
2458
|
+
]
|
|
2459
|
+
}["a"][1];
|
|
2460
|
+
`,
|
|
2461
|
+
},
|
|
2462
|
+
{
|
|
2463
|
+
name: 'Array',
|
|
2464
|
+
source: dedent`
|
|
2465
|
+
type DeepAccess = {
|
|
2466
|
+
a: { y: { c: number[] }["c"] }["y"][]
|
|
2467
|
+
}["a"][1];
|
|
2468
|
+
`,
|
|
2469
|
+
expected: dedent`
|
|
2470
|
+
type DeepAccess = {
|
|
2471
|
+
a: { y: { c: readonly number[] }["c"] }["y"][]
|
|
2472
|
+
}["a"][1];
|
|
2473
|
+
`,
|
|
2474
|
+
},
|
|
2475
|
+
{
|
|
2476
|
+
name: 'MappedType',
|
|
2477
|
+
source: dedent`
|
|
2478
|
+
type Obj = { a: number, b: number, c: number };
|
|
2479
|
+
type IndexAccessOnMappedType = { [key in keyof Obj]: number[] }["a"];
|
|
2480
|
+
`,
|
|
2481
|
+
expected: dedent`
|
|
2482
|
+
type Obj = Readonly<{ a: number, b: number, c: number }>;
|
|
2483
|
+
type IndexAccessOnMappedType = { [key in keyof Obj]: readonly number[] }["a"];
|
|
2484
|
+
`,
|
|
2485
|
+
},
|
|
2486
|
+
// DeepReadonly pattern tests
|
|
2487
|
+
{
|
|
2488
|
+
name: 'DeepReadonly with IndexedAccessType',
|
|
2489
|
+
source: dedent`
|
|
2490
|
+
type DeepObj = DeepReadonly<{ a: { b: number[] } }>;
|
|
2491
|
+
type AccessDeep = DeepObj["a"]["b"];
|
|
2492
|
+
`,
|
|
2493
|
+
expected: dedent`
|
|
2494
|
+
type DeepObj = DeepReadonly<{ a: { b: number[] } }>;
|
|
2495
|
+
type AccessDeep = DeepObj["a"]["b"];
|
|
2496
|
+
`,
|
|
2497
|
+
},
|
|
2498
|
+
{
|
|
2499
|
+
name: 'DeepReadonly with nested IndexedAccessType',
|
|
2500
|
+
source: dedent`
|
|
2501
|
+
type Config = {
|
|
2502
|
+
settings: {
|
|
2503
|
+
options: { values: number[] }
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
type DeepConfig = DeepReadonly<Config>;
|
|
2507
|
+
type AccessDeepNested = DeepConfig["settings"]["options"]["values"];
|
|
2508
|
+
`,
|
|
2509
|
+
expected: dedent`
|
|
2510
|
+
type Config = Readonly<{
|
|
2511
|
+
settings: Readonly<{
|
|
2512
|
+
options: Readonly<{ values: readonly number[] }>
|
|
2513
|
+
}>
|
|
2514
|
+
}>;
|
|
2515
|
+
type DeepConfig = DeepReadonly<Config>;
|
|
2516
|
+
type AccessDeepNested = DeepConfig["settings"]["options"]["values"];
|
|
2517
|
+
`,
|
|
2518
|
+
},
|
|
2519
|
+
// Union/Intersection with IndexedAccessType
|
|
2520
|
+
{
|
|
2521
|
+
name: 'Union with IndexedAccessType (inline)',
|
|
2522
|
+
source: dedent`
|
|
2523
|
+
type UnionAccess = ({ x: number[] } | { x: string[] })["x"];
|
|
2524
|
+
`,
|
|
2525
|
+
expected: dedent`
|
|
2526
|
+
type UnionAccess = ({ x: readonly number[] } | { x: readonly string[] })["x"];
|
|
2527
|
+
`,
|
|
2528
|
+
},
|
|
2529
|
+
{
|
|
2530
|
+
name: 'Intersection with IndexedAccessType (inline)',
|
|
2531
|
+
source: dedent`
|
|
2532
|
+
type IntersectionAccess = ({ x: number[]; shared: boolean[] } & { y: string[]; shared: boolean[] })["shared"];
|
|
2533
|
+
`,
|
|
2534
|
+
expected: dedent`
|
|
2535
|
+
type IntersectionAccess = ({ x: readonly number[]; shared: readonly boolean[] } & { y: readonly string[]; shared: readonly boolean[] })["shared"];
|
|
2536
|
+
`,
|
|
2537
|
+
},
|
|
2538
|
+
{
|
|
2539
|
+
name: 'Complex Union/Intersection with IndexedAccessType (inline)',
|
|
2540
|
+
source: dedent`
|
|
2541
|
+
type ComplexAccess = (({ x: number[] } | { y: string[] }) & ({ z: boolean[] } | { w: object[] }))["z" | "w"];
|
|
2542
|
+
`,
|
|
2543
|
+
expected: dedent`
|
|
2544
|
+
type ComplexAccess = (({ x: readonly number[] } | { y: readonly string[] }) & ({ z: readonly boolean[] } | { w: readonly object[] }))["z" | "w"];
|
|
2545
|
+
`,
|
|
2546
|
+
},
|
|
2547
|
+
// Named tuple with IndexedAccessType
|
|
2548
|
+
{
|
|
2549
|
+
name: 'Named tuple with IndexedAccessType (inline)',
|
|
2550
|
+
source: dedent`
|
|
2551
|
+
type AccessNamedTuple = [first: string, second: number[], third: { x: boolean[] }][1];
|
|
2552
|
+
`,
|
|
2553
|
+
expected: dedent`
|
|
2554
|
+
type AccessNamedTuple = [first: string, second: readonly number[], third: Readonly<{ x: readonly boolean[] }>][1];
|
|
2555
|
+
`,
|
|
2556
|
+
},
|
|
2557
|
+
{
|
|
2558
|
+
name: 'Complex named tuple with IndexedAccessType (inline)',
|
|
2559
|
+
source: dedent`
|
|
2560
|
+
type AccessComplexTuple = [
|
|
2561
|
+
id: string,
|
|
2562
|
+
data: { values: number[] }[],
|
|
2563
|
+
metadata: [owner: string, permissions: string[]]
|
|
2564
|
+
][2][1];
|
|
2565
|
+
`,
|
|
2566
|
+
expected: dedent`
|
|
2567
|
+
type AccessComplexTuple = [
|
|
2568
|
+
id: string,
|
|
2569
|
+
data: Readonly<{ values: readonly number[] }>[],
|
|
2570
|
+
metadata: [owner: string, permissions: readonly string[]]
|
|
2571
|
+
][2][1];
|
|
2572
|
+
`,
|
|
2573
|
+
},
|
|
2574
|
+
// Additional patterns
|
|
2575
|
+
{
|
|
2576
|
+
name: 'Conditional type with IndexedAccessType (inline)',
|
|
2577
|
+
source: dedent`
|
|
2578
|
+
type Result = ({ data: number[] } extends { data: infer U } ? U : never);
|
|
2579
|
+
`,
|
|
2580
|
+
expected: dedent`
|
|
2581
|
+
type Result = (Readonly<{ data: readonly number[] }> extends Readonly<{ data: infer U }> ? U : never);
|
|
2582
|
+
`,
|
|
2583
|
+
},
|
|
2584
|
+
{
|
|
2585
|
+
name: 'Template literal with IndexedAccessType (inline)',
|
|
2586
|
+
source: dedent`
|
|
2587
|
+
type UserParams = {
|
|
2588
|
+
"/users": { params: string[] },
|
|
2589
|
+
"/posts": { params: number[] }
|
|
2590
|
+
}["/users"]["params"];
|
|
2591
|
+
`,
|
|
2592
|
+
expected: dedent`
|
|
2593
|
+
type UserParams = {
|
|
2594
|
+
"/users": { params: readonly string[] },
|
|
2595
|
+
"/posts": { params: readonly number[] }
|
|
2596
|
+
}["/users"]["params"];
|
|
2597
|
+
`,
|
|
2598
|
+
},
|
|
2599
|
+
])('$name', testFn);
|
|
2600
|
+
});
|
|
2601
|
+
|
|
2602
|
+
describe('Function and constructor types', () => {
|
|
2603
|
+
test.each([
|
|
2604
|
+
{
|
|
2605
|
+
name: 'Function type alias',
|
|
2606
|
+
source: dedent`
|
|
2607
|
+
type MyFunc = (arg: number[]) => string[]
|
|
2608
|
+
`,
|
|
2609
|
+
expected: dedent`
|
|
2610
|
+
type MyFunc = (arg: readonly number[]) => readonly string[];
|
|
2611
|
+
`,
|
|
2612
|
+
},
|
|
2613
|
+
{
|
|
2614
|
+
name: 'Function type variable',
|
|
2615
|
+
source: dedent`
|
|
2616
|
+
let fn: (map: Map<string, number>) => Set<string[]>
|
|
2617
|
+
`,
|
|
2618
|
+
expected: dedent`
|
|
2619
|
+
let fn: (map: ReadonlyMap<string, number>) => ReadonlySet<readonly string[]>;
|
|
2620
|
+
`,
|
|
2621
|
+
},
|
|
2622
|
+
{
|
|
2623
|
+
name: 'Constructor type alias',
|
|
2624
|
+
source: dedent`
|
|
2625
|
+
type MyConstructor = new (arg: string[]) => { prop: number[] };
|
|
2626
|
+
`,
|
|
2627
|
+
expected: dedent`
|
|
2628
|
+
type MyConstructor = new (arg: readonly string[]) => Readonly<{ prop: readonly number[] }>;
|
|
2629
|
+
`,
|
|
2630
|
+
},
|
|
2631
|
+
])('$name', testFn);
|
|
2632
|
+
});
|
|
2633
|
+
|
|
2634
|
+
describe('Generic Types', () => {
|
|
2635
|
+
test.each([
|
|
2636
|
+
{
|
|
2637
|
+
name: 'Generic type parameter in interface',
|
|
2638
|
+
source: dedent`
|
|
2639
|
+
interface GenericInterface<T> { data: T[]; }
|
|
2640
|
+
`,
|
|
2641
|
+
expected: dedent`
|
|
2642
|
+
interface GenericInterface<T> { readonly data: readonly T[]; }
|
|
2643
|
+
`,
|
|
2644
|
+
},
|
|
2645
|
+
{
|
|
2646
|
+
name: 'Generic type parameter in type alias',
|
|
2647
|
+
source: dedent`
|
|
2648
|
+
type GenericType<T> = { data: T[]; }
|
|
2649
|
+
`,
|
|
2650
|
+
expected: dedent`
|
|
2651
|
+
type GenericType<T> = Readonly<{ data: readonly T[]; }>
|
|
2652
|
+
`,
|
|
2653
|
+
},
|
|
2654
|
+
{
|
|
2655
|
+
name: 'Multiple generic type parameters',
|
|
2656
|
+
source: dedent`
|
|
2657
|
+
type Pair<K, V> = { key: K; value: V[]; }
|
|
2658
|
+
`,
|
|
2659
|
+
expected: dedent`
|
|
2660
|
+
type Pair<K, V> = Readonly<{ key: K; value: readonly V[]; }>;
|
|
2661
|
+
`,
|
|
2662
|
+
},
|
|
2663
|
+
{
|
|
2664
|
+
name: 'Generic type with constraints',
|
|
2665
|
+
source: dedent`
|
|
2666
|
+
type NumberContainer<T extends number> = { values: T[]; }
|
|
2667
|
+
`,
|
|
2668
|
+
expected: dedent`
|
|
2669
|
+
type NumberContainer<T extends number> = Readonly<{ values: readonly T[]; }>;
|
|
2670
|
+
`,
|
|
2671
|
+
},
|
|
2672
|
+
{
|
|
2673
|
+
name: 'Generic type with default',
|
|
2674
|
+
source: dedent`
|
|
2675
|
+
type Container<T = string> = { items: T[]; }
|
|
2676
|
+
`,
|
|
2677
|
+
expected: dedent`
|
|
2678
|
+
type Container<T = string> = Readonly<{ items: readonly T[]; }>;
|
|
2679
|
+
`,
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
name: 'Nested generic types',
|
|
2683
|
+
source: dedent`
|
|
2684
|
+
type NestedContainer<T> = { outer: Array<{ inner: T[] }>; }
|
|
2685
|
+
`,
|
|
2686
|
+
expected: dedent`
|
|
2687
|
+
type NestedContainer<T> = Readonly<{ outer: readonly Readonly<{ inner: readonly T[]; }>[]; }>;
|
|
2688
|
+
`,
|
|
2689
|
+
},
|
|
2690
|
+
{
|
|
2691
|
+
name: 'Generic type with union',
|
|
2692
|
+
source: dedent`
|
|
2693
|
+
type Result<T, E> = { data: T; } | { error: E; }
|
|
2694
|
+
`,
|
|
2695
|
+
expected: dedent`
|
|
2696
|
+
type Result<T, E> = Readonly<{ data: T; } | { error: E; }>
|
|
2697
|
+
`,
|
|
2698
|
+
},
|
|
2699
|
+
{
|
|
2700
|
+
name: 'Generic type with intersection',
|
|
2701
|
+
source: dedent`
|
|
2702
|
+
type WithId<T> = { id: string; } & T
|
|
2703
|
+
`,
|
|
2704
|
+
expected: dedent`
|
|
2705
|
+
type WithId<T> = Readonly<{ id: string; }> & T
|
|
2706
|
+
`,
|
|
2707
|
+
},
|
|
2708
|
+
{
|
|
2709
|
+
name: 'Generic DeepReadonly usage',
|
|
2710
|
+
source: dedent`
|
|
2711
|
+
type DeepReadonlyGeneric<T> = DeepReadonly<T>
|
|
2712
|
+
`,
|
|
2713
|
+
expected: dedent`
|
|
2714
|
+
type DeepReadonlyGeneric<T> = DeepReadonly<T>
|
|
2715
|
+
`,
|
|
2716
|
+
},
|
|
2717
|
+
{
|
|
2718
|
+
name: 'Complex generic with conditional types',
|
|
2719
|
+
source: dedent`
|
|
2720
|
+
type MaybeArray<T> = T extends any[] ? T : T[]
|
|
2721
|
+
`,
|
|
2722
|
+
expected: dedent`
|
|
2723
|
+
type MaybeArray<T> = T extends readonly any[] ? T : readonly T[];
|
|
2724
|
+
`,
|
|
2725
|
+
},
|
|
2726
|
+
])('$name', testFn);
|
|
2727
|
+
});
|
|
2728
|
+
|
|
2729
|
+
describe('Canonical readonly forms are stable', () => {
|
|
2730
|
+
// Verify canonical forms are stable
|
|
2731
|
+
test.each([
|
|
2732
|
+
{
|
|
2733
|
+
name: 'readonly T[] is unchanged',
|
|
2734
|
+
source: dedent`
|
|
2735
|
+
type T = readonly string[]
|
|
2736
|
+
`,
|
|
2737
|
+
expected: dedent`
|
|
2738
|
+
type T = readonly string[]
|
|
2739
|
+
`,
|
|
2740
|
+
},
|
|
2741
|
+
{
|
|
2742
|
+
name: 'readonly [T1, T2] is unchanged',
|
|
2743
|
+
source: dedent`
|
|
2744
|
+
type T = readonly [string, number]
|
|
2745
|
+
`,
|
|
2746
|
+
expected: dedent`
|
|
2747
|
+
type T = readonly [string, number]
|
|
2748
|
+
`,
|
|
2749
|
+
},
|
|
2750
|
+
{
|
|
2751
|
+
name: 'readonly [T1, ...T2[]] is unchanged',
|
|
2752
|
+
source: dedent`
|
|
2753
|
+
type T = readonly [string, ...number[]]
|
|
2754
|
+
`,
|
|
2755
|
+
expected: dedent`
|
|
2756
|
+
type T = readonly [string, ...number[]]
|
|
2757
|
+
`,
|
|
2758
|
+
},
|
|
2759
|
+
{
|
|
2760
|
+
name: 'ReadonlySet<T> is unchanged',
|
|
2761
|
+
source: dedent`
|
|
2762
|
+
type T = ReadonlySet<number>
|
|
2763
|
+
`,
|
|
2764
|
+
expected: dedent`
|
|
2765
|
+
type T = ReadonlySet<number>
|
|
2766
|
+
`,
|
|
2767
|
+
},
|
|
2768
|
+
{
|
|
2769
|
+
name: 'ReadonlyMap<K, V> is unchanged',
|
|
2770
|
+
source: dedent`
|
|
2771
|
+
type T = ReadonlyMap<string, boolean>
|
|
2772
|
+
`,
|
|
2773
|
+
expected: dedent`
|
|
2774
|
+
type T = ReadonlyMap<string, boolean>
|
|
2775
|
+
`,
|
|
2776
|
+
},
|
|
2777
|
+
{
|
|
2778
|
+
name: 'Readonly<{ a: T }> is unchanged',
|
|
2779
|
+
source: dedent`
|
|
2780
|
+
type T = Readonly<{ a: string }>
|
|
2781
|
+
`,
|
|
2782
|
+
expected: dedent`
|
|
2783
|
+
type T = Readonly<{ a: string }>
|
|
2784
|
+
`,
|
|
2785
|
+
},
|
|
2786
|
+
{
|
|
2787
|
+
name: 'Interface with readonly member is unchanged // Interface itself should not be transformed',
|
|
2788
|
+
source: dedent`
|
|
2789
|
+
interface I { readonly prop: number; }
|
|
2790
|
+
`,
|
|
2791
|
+
expected: dedent`
|
|
2792
|
+
interface I { readonly prop: number; }
|
|
2793
|
+
`,
|
|
2794
|
+
},
|
|
2795
|
+
{
|
|
2796
|
+
name: 'Complex nested readonly is unchanged',
|
|
2797
|
+
source: dedent`
|
|
2798
|
+
type T = ReadonlyMap<string, readonly Readonly<{ p: readonly boolean[] }>[]>;
|
|
2799
|
+
`,
|
|
2800
|
+
expected: dedent`
|
|
2801
|
+
type T = ReadonlyMap<string, readonly Readonly<{ p: readonly boolean[] }>[]>;
|
|
2802
|
+
`,
|
|
2803
|
+
},
|
|
2804
|
+
])('$name', testFn);
|
|
2805
|
+
});
|
|
2806
|
+
|
|
2807
|
+
describe('Other syntax elements', () => {
|
|
2808
|
+
test.each([
|
|
2809
|
+
{
|
|
2810
|
+
name: 'Typeof on array variable',
|
|
2811
|
+
source: dedent`
|
|
2812
|
+
const arr = [1, 2];
|
|
2813
|
+
type ArrType = typeof arr;
|
|
2814
|
+
`,
|
|
2815
|
+
expected: dedent`
|
|
2816
|
+
const arr = [1, 2];
|
|
2817
|
+
type ArrType = typeof arr;
|
|
2818
|
+
`,
|
|
2819
|
+
},
|
|
2820
|
+
{
|
|
2821
|
+
name: 'Typeof on object variable',
|
|
2822
|
+
source: dedent`
|
|
2823
|
+
const obj = { data: [1] };
|
|
2824
|
+
type ObjType = typeof obj;
|
|
2825
|
+
`,
|
|
2826
|
+
expected: dedent`
|
|
2827
|
+
const obj = { data: [1] };
|
|
2828
|
+
type ObjType = typeof obj;
|
|
2829
|
+
`,
|
|
2830
|
+
},
|
|
2831
|
+
{
|
|
2832
|
+
name: 'Indexed access type',
|
|
2833
|
+
source: dedent`
|
|
2834
|
+
type PropType<T, K extends keyof T> = T[K];
|
|
2835
|
+
type A = PropType<{ p: number[] }, "p">;
|
|
2836
|
+
`,
|
|
2837
|
+
expected: dedent`
|
|
2838
|
+
type PropType<T, K extends keyof T> = T[K];
|
|
2839
|
+
type A = PropType<Readonly<{ p: readonly number[] }>, "p">;
|
|
2840
|
+
`,
|
|
2841
|
+
},
|
|
2842
|
+
{
|
|
2843
|
+
name: 'Indexed access type with array/tuple',
|
|
2844
|
+
source: dedent`
|
|
2845
|
+
type ElementType<T extends any[]> = T[number]; type B = ElementType<string[][]>;
|
|
2846
|
+
`,
|
|
2847
|
+
expected: dedent`
|
|
2848
|
+
type ElementType<T extends readonly any[]> = T[number]; type B = ElementType<readonly (readonly string[])[]>;
|
|
2849
|
+
`,
|
|
2850
|
+
},
|
|
2851
|
+
{
|
|
2852
|
+
name: 'Conditional type',
|
|
2853
|
+
source: dedent`
|
|
2854
|
+
type Check<T> = T extends string[] ? { value: T } : { error: Error }; type C = Check<number[][]>;
|
|
2855
|
+
`,
|
|
2856
|
+
expected: dedent`
|
|
2857
|
+
type Check<T> = T extends readonly string[] ? Readonly<{ value: T }> : Readonly<{ error: Error }>; type C = Check<readonly (readonly number[])[]>;
|
|
2858
|
+
`,
|
|
2859
|
+
},
|
|
2860
|
+
{
|
|
2861
|
+
name: 'Conditional type with complex branches',
|
|
2862
|
+
source: dedent`
|
|
2863
|
+
type ComplexCondition<T> = T extends Map<string, number[]> ? Set<T>[] : Array<Map<string, T>>;
|
|
2864
|
+
`,
|
|
2865
|
+
expected: dedent`
|
|
2866
|
+
type ComplexCondition<T> = T extends ReadonlyMap<string, readonly number[]> ? readonly ReadonlySet<T>[] : readonly ReadonlyMap<string, T>[];
|
|
2867
|
+
`,
|
|
2868
|
+
},
|
|
2869
|
+
{
|
|
2870
|
+
name: 'Conditional type with infer',
|
|
2871
|
+
source: dedent`
|
|
2872
|
+
type InferArrayItem<T> = T extends (infer I)[] ? { item: I[] } : never; type D = InferArrayItem<Date[]>;
|
|
2873
|
+
`,
|
|
2874
|
+
expected: dedent`
|
|
2875
|
+
type InferArrayItem<T> = T extends readonly (infer I)[] ? Readonly<{ item: readonly I[] }> : never; type D = InferArrayItem<readonly Date[]>;
|
|
2876
|
+
`,
|
|
2877
|
+
},
|
|
2878
|
+
{
|
|
2879
|
+
name: 'Conditional type with infer in return position',
|
|
2880
|
+
source: dedent`
|
|
2881
|
+
type UnwrapArray<T> = T extends Array<infer U> ? U[] : T
|
|
2882
|
+
`,
|
|
2883
|
+
|
|
2884
|
+
expected: dedent`
|
|
2885
|
+
type UnwrapArray<T> = T extends readonly (infer U)[] ? readonly U[] : T;
|
|
2886
|
+
`,
|
|
2887
|
+
},
|
|
2888
|
+
{
|
|
2889
|
+
name: 'Conditional type with infer used in object',
|
|
2890
|
+
source: dedent`
|
|
2891
|
+
type InferToObject<T> = T extends Set<infer I> ? { item: I[] } : never;
|
|
2892
|
+
`,
|
|
2893
|
+
expected: dedent`
|
|
2894
|
+
type InferToObject<T> = T extends ReadonlySet<infer I> ? Readonly<{ item: readonly I[] }> : never;
|
|
2895
|
+
`,
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
name: 'Type assertion <T>',
|
|
2899
|
+
source: dedent`
|
|
2900
|
+
let x = <Map<string, number[]>>{}
|
|
2901
|
+
`,
|
|
2902
|
+
expected: dedent`
|
|
2903
|
+
let x = <ReadonlyMap<string, readonly number[]>>{}
|
|
2904
|
+
`,
|
|
2905
|
+
},
|
|
2906
|
+
{
|
|
2907
|
+
name: 'Recursive type alias (linked list)',
|
|
2908
|
+
source: dedent`
|
|
2909
|
+
type List<T> = { value: T, next: List<T> | null };
|
|
2910
|
+
let list: List<number[]>;
|
|
2911
|
+
`,
|
|
2912
|
+
expected: dedent`
|
|
2913
|
+
type List<T> = Readonly<{ value: T, next: List<T> | null }>;
|
|
2914
|
+
let list: List<readonly number[]>;
|
|
2915
|
+
`,
|
|
2916
|
+
},
|
|
2917
|
+
{
|
|
2918
|
+
name: 'Recursive type alias (tree)',
|
|
2919
|
+
source: dedent`
|
|
2920
|
+
type Tree<T> = { value: T; children: Tree<T>[] }; let tree: Tree<{ id: number[] }>;
|
|
2921
|
+
`,
|
|
2922
|
+
expected: dedent`
|
|
2923
|
+
type Tree<T> = Readonly<{ value: T; children: readonly Tree<T>[] }>; let tree: Tree<Readonly<{ id: readonly number[] }>>;
|
|
2924
|
+
`,
|
|
2925
|
+
},
|
|
2926
|
+
{
|
|
2927
|
+
name: 'Code with comments near types',
|
|
2928
|
+
source: dedent`
|
|
2929
|
+
// Single line comment
|
|
2930
|
+
type /* Block Comment */ WithComments = {
|
|
2931
|
+
prop1: string[]; // Trailing comment
|
|
2932
|
+
/* Another block */
|
|
2933
|
+
prop2: /* member type comment */ Readonly< /* member type inner comment */ number[]>;
|
|
2934
|
+
/* End block */
|
|
2935
|
+
};
|
|
2936
|
+
`,
|
|
2937
|
+
expected: dedent`
|
|
2938
|
+
// Single line comment
|
|
2939
|
+
type /* Block Comment */ WithComments = Readonly<{
|
|
2940
|
+
prop1: readonly string[]; // Trailing comment
|
|
2941
|
+
/* Another block */
|
|
2942
|
+
prop2: /* member type comment */ readonly /* member type inner comment */ number[];
|
|
2943
|
+
/* End block */
|
|
2944
|
+
}>;
|
|
2945
|
+
`,
|
|
2946
|
+
},
|
|
2947
|
+
|
|
2948
|
+
{
|
|
2949
|
+
name: 'Class implements clause',
|
|
2950
|
+
source: dedent`
|
|
2951
|
+
interface IBox<T> { value: T[]; }
|
|
2952
|
+
class BoxImpl implements IBox<string[]> {
|
|
2953
|
+
value: string[];
|
|
2954
|
+
constructor() { this.value = []; }
|
|
2955
|
+
}
|
|
2956
|
+
`,
|
|
2957
|
+
expected: dedent`
|
|
2958
|
+
interface IBox<T> { readonly value: readonly T[]; }
|
|
2959
|
+
class BoxImpl implements IBox<readonly string[]> {
|
|
2960
|
+
readonly value: readonly string[];
|
|
2961
|
+
constructor() { this.value = []; }
|
|
2962
|
+
}
|
|
2963
|
+
`,
|
|
2964
|
+
},
|
|
2965
|
+
{
|
|
2966
|
+
name: 'Class extends with generics',
|
|
2967
|
+
source: dedent`
|
|
2968
|
+
class Base<T> { item: T[] }
|
|
2969
|
+
class Derived extends Base<string[]> {}
|
|
2970
|
+
`,
|
|
2971
|
+
expected: dedent`
|
|
2972
|
+
class Base<T> { readonly item: readonly T[] }
|
|
2973
|
+
class Derived extends Base<readonly string[]> {}
|
|
2974
|
+
`,
|
|
2975
|
+
},
|
|
2976
|
+
{
|
|
2977
|
+
name: 'Function type variable',
|
|
2978
|
+
source: dedent`
|
|
2979
|
+
let foo: () => number
|
|
2980
|
+
`,
|
|
2981
|
+
expected: dedent`
|
|
2982
|
+
let foo: () => number
|
|
2983
|
+
`,
|
|
2984
|
+
},
|
|
2985
|
+
{
|
|
2986
|
+
name: 'Destructuring assignment with tuple type',
|
|
2987
|
+
source: dedent`
|
|
2988
|
+
const [x, y]: [number[], number[]] = [[1, 2, 3], [4, 5, 6]]
|
|
2989
|
+
`,
|
|
2990
|
+
expected: dedent`
|
|
2991
|
+
const [x, y]: readonly [readonly number[], readonly number[]] = [[1, 2, 3], [4, 5, 6]];
|
|
2992
|
+
`,
|
|
2993
|
+
},
|
|
2994
|
+
{
|
|
2995
|
+
name: 'Namespace containing type alias',
|
|
2996
|
+
source: dedent`
|
|
2997
|
+
namespace X {
|
|
2998
|
+
type TypeAlias = {
|
|
2999
|
+
a: number[]
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
`,
|
|
3003
|
+
expected: dedent`
|
|
3004
|
+
namespace X {
|
|
3005
|
+
type TypeAlias = Readonly<{
|
|
3006
|
+
a: readonly number[]
|
|
3007
|
+
}>;
|
|
3008
|
+
}
|
|
3009
|
+
`,
|
|
3010
|
+
},
|
|
3011
|
+
{
|
|
3012
|
+
name: 'Module containing type alias',
|
|
3013
|
+
source: dedent`
|
|
3014
|
+
module X {
|
|
3015
|
+
type TypeAlias = {
|
|
3016
|
+
a: number[]
|
|
3017
|
+
};
|
|
3018
|
+
}
|
|
3019
|
+
`,
|
|
3020
|
+
expected: dedent`
|
|
3021
|
+
module X {
|
|
3022
|
+
type TypeAlias = Readonly<{
|
|
3023
|
+
a: readonly number[]
|
|
3024
|
+
}>;
|
|
3025
|
+
}
|
|
3026
|
+
`,
|
|
3027
|
+
},
|
|
3028
|
+
])('$name', testFn);
|
|
3029
|
+
});
|
|
3030
|
+
|
|
3031
|
+
describe('ignorePrefixes option', () => {
|
|
3032
|
+
test.each([
|
|
3033
|
+
{
|
|
3034
|
+
name: 'Variable declarations',
|
|
3035
|
+
source: dedent`
|
|
3036
|
+
const mut_foo: string[] = []
|
|
3037
|
+
`,
|
|
3038
|
+
expected: dedent`
|
|
3039
|
+
const mut_foo: string[] = []
|
|
3040
|
+
`,
|
|
3041
|
+
},
|
|
3042
|
+
{
|
|
3043
|
+
name: 'Function declarations',
|
|
3044
|
+
source: dedent`
|
|
3045
|
+
function mut_foo(a: readonly number[], b: Promise<number[]>) {}
|
|
3046
|
+
`,
|
|
3047
|
+
expected: dedent`
|
|
3048
|
+
function mut_foo(a: readonly number[], b: Promise<number[]>) {}
|
|
3049
|
+
`,
|
|
3050
|
+
},
|
|
3051
|
+
{
|
|
3052
|
+
name: 'Type aliases',
|
|
3053
|
+
source: dedent`
|
|
3054
|
+
type mut_Foo = number[]
|
|
3055
|
+
`,
|
|
3056
|
+
expected: dedent`
|
|
3057
|
+
type mut_Foo = number[]
|
|
3058
|
+
`,
|
|
3059
|
+
},
|
|
3060
|
+
{
|
|
3061
|
+
name: 'Ignore variable with prefix',
|
|
3062
|
+
source: dedent`
|
|
3063
|
+
type T = { mut_value: number[]; normalValue: string[] }
|
|
3064
|
+
`,
|
|
3065
|
+
expected: dedent`
|
|
3066
|
+
type T = Readonly<{
|
|
3067
|
+
mut_value: number[];
|
|
3068
|
+
normalValue: readonly string[];
|
|
3069
|
+
}>;
|
|
3070
|
+
`,
|
|
3071
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3072
|
+
},
|
|
3073
|
+
{
|
|
3074
|
+
name: 'Multiple ignore prefixes',
|
|
3075
|
+
source: dedent`
|
|
3076
|
+
type T = { mut_value: number[]; mutable_data: string[]; normalValue: boolean[] };
|
|
3077
|
+
`,
|
|
3078
|
+
expected: dedent`
|
|
3079
|
+
type T = Readonly<{
|
|
3080
|
+
mut_value: number[];
|
|
3081
|
+
mutable_data: string[];
|
|
3082
|
+
normalValue: readonly boolean[];
|
|
3083
|
+
}>;
|
|
3084
|
+
`,
|
|
3085
|
+
options: { ignorePrefixes: ['mut_', 'mutable_'] },
|
|
3086
|
+
},
|
|
3087
|
+
{
|
|
3088
|
+
name: 'Ignore prefix in nested properties',
|
|
3089
|
+
source: dedent`
|
|
3090
|
+
type T = {
|
|
3091
|
+
data: {
|
|
3092
|
+
mut_items: number[];
|
|
3093
|
+
items: string[];
|
|
3094
|
+
};
|
|
3095
|
+
};
|
|
3096
|
+
`,
|
|
3097
|
+
expected: dedent`
|
|
3098
|
+
type T = Readonly<{
|
|
3099
|
+
data: Readonly<{
|
|
3100
|
+
mut_items: number[];
|
|
3101
|
+
items: readonly string[];
|
|
3102
|
+
}>;
|
|
3103
|
+
}>;
|
|
3104
|
+
`,
|
|
3105
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3106
|
+
},
|
|
3107
|
+
{
|
|
3108
|
+
name: 'Ignore prefix in interface',
|
|
3109
|
+
source: dedent`
|
|
3110
|
+
interface User {
|
|
3111
|
+
mut_permissions: string[];
|
|
3112
|
+
roles: string[];
|
|
3113
|
+
}
|
|
3114
|
+
`,
|
|
3115
|
+
expected: dedent`
|
|
3116
|
+
interface User {
|
|
3117
|
+
mut_permissions: string[];
|
|
3118
|
+
readonly roles: readonly string[];
|
|
3119
|
+
}
|
|
3120
|
+
`,
|
|
3121
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3122
|
+
},
|
|
3123
|
+
{
|
|
3124
|
+
name: 'Ignore prefixes for various name node types in interface',
|
|
3125
|
+
source: dedent`
|
|
3126
|
+
interface I {
|
|
3127
|
+
mut_a: string[]; // Identifier
|
|
3128
|
+
#mut_x: string[]; // PrivateIdentifier
|
|
3129
|
+
"mut_b": string[]; // StringLiteral
|
|
3130
|
+
['mut_c']: string[]; // ComputedPropertyName with StringLiteral
|
|
3131
|
+
['mu' + 't_d']: string[]; // ComputedPropertyName
|
|
3132
|
+
}
|
|
3133
|
+
`,
|
|
3134
|
+
expected: dedent`
|
|
3135
|
+
interface I {
|
|
3136
|
+
mut_a: string[]; // Identifier
|
|
3137
|
+
#mut_x: string[]; // PrivateIdentifier
|
|
3138
|
+
"mut_b": string[]; // StringLiteral
|
|
3139
|
+
['mut_c']: string[]; // ComputedPropertyName with StringLiteral
|
|
3140
|
+
readonly ['mu' + 't_d']: readonly string[]; // ComputedPropertyName
|
|
3141
|
+
}
|
|
3142
|
+
`,
|
|
3143
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3144
|
+
},
|
|
3145
|
+
{
|
|
3146
|
+
name: 'Ignore prefix with index signature',
|
|
3147
|
+
source: dedent`
|
|
3148
|
+
type T = {
|
|
3149
|
+
mut_items: {
|
|
3150
|
+
[key: string]: number[];
|
|
3151
|
+
};
|
|
3152
|
+
};
|
|
3153
|
+
`,
|
|
3154
|
+
expected: dedent`
|
|
3155
|
+
type T = Readonly<{
|
|
3156
|
+
mut_items: {
|
|
3157
|
+
[key: string]: number[];
|
|
3158
|
+
};
|
|
3159
|
+
}>;
|
|
3160
|
+
`,
|
|
3161
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3162
|
+
},
|
|
3163
|
+
{
|
|
3164
|
+
name: 'Index signature in ignored property',
|
|
3165
|
+
source: dedent`
|
|
3166
|
+
interface Store {
|
|
3167
|
+
mut_data: {
|
|
3168
|
+
[key: string]: string[];
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
`,
|
|
3172
|
+
expected: dedent`
|
|
3173
|
+
interface Store {
|
|
3174
|
+
mut_data: {
|
|
3175
|
+
[key: string]: string[];
|
|
3176
|
+
};
|
|
3177
|
+
}
|
|
3178
|
+
`,
|
|
3179
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3180
|
+
},
|
|
3181
|
+
{
|
|
3182
|
+
name: 'Type alias with ignored prefix',
|
|
3183
|
+
source: dedent`
|
|
3184
|
+
type mut_Config = {
|
|
3185
|
+
items: string[];
|
|
3186
|
+
};
|
|
3187
|
+
`,
|
|
3188
|
+
expected: dedent`
|
|
3189
|
+
type mut_Config = {
|
|
3190
|
+
items: string[];
|
|
3191
|
+
};
|
|
3192
|
+
`,
|
|
3193
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3194
|
+
},
|
|
3195
|
+
{
|
|
3196
|
+
name: 'Function with ignored prefix',
|
|
3197
|
+
source: dedent`
|
|
3198
|
+
function mut_process(data: string[]): number[] { return []; }
|
|
3199
|
+
`,
|
|
3200
|
+
expected: dedent`
|
|
3201
|
+
function mut_process(data: string[]): number[] { return []; }
|
|
3202
|
+
`,
|
|
3203
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3204
|
+
},
|
|
3205
|
+
{
|
|
3206
|
+
name: 'Class with ignored prefix',
|
|
3207
|
+
source: dedent`
|
|
3208
|
+
class mut_Store {
|
|
3209
|
+
items: string[] = [];
|
|
3210
|
+
}
|
|
3211
|
+
`,
|
|
3212
|
+
expected: dedent`
|
|
3213
|
+
class mut_Store {
|
|
3214
|
+
items: string[] = [];
|
|
3215
|
+
}
|
|
3216
|
+
`,
|
|
3217
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3218
|
+
},
|
|
3219
|
+
{
|
|
3220
|
+
name: 'Variable declaration with ignored prefix',
|
|
3221
|
+
source: dedent`
|
|
3222
|
+
const mut_items: string[] = []
|
|
3223
|
+
`,
|
|
3224
|
+
expected: dedent`
|
|
3225
|
+
const mut_items: string[] = []
|
|
3226
|
+
`,
|
|
3227
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3228
|
+
},
|
|
3229
|
+
{
|
|
3230
|
+
name: 'Generic type with ignored prefix',
|
|
3231
|
+
source: dedent`
|
|
3232
|
+
type mut_List<T> = {
|
|
3233
|
+
items: T[]
|
|
3234
|
+
}
|
|
3235
|
+
`,
|
|
3236
|
+
expected: dedent`
|
|
3237
|
+
type mut_List<T> = {
|
|
3238
|
+
items: T[];
|
|
3239
|
+
};
|
|
3240
|
+
`,
|
|
3241
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
name: 'Union type with ignored prefix members',
|
|
3245
|
+
source: dedent`
|
|
3246
|
+
type Result = mut_Success | Error;
|
|
3247
|
+
type mut_Success = {
|
|
3248
|
+
data: string[]
|
|
3249
|
+
};
|
|
3250
|
+
`,
|
|
3251
|
+
expected: dedent`
|
|
3252
|
+
type Result = mut_Success | Error;
|
|
3253
|
+
type mut_Success = {
|
|
3254
|
+
data: string[];
|
|
3255
|
+
};
|
|
3256
|
+
`,
|
|
3257
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
name: 'Property with ignored prefix in class',
|
|
3261
|
+
source: dedent`
|
|
3262
|
+
class Store {
|
|
3263
|
+
mut_items: string[] = [];
|
|
3264
|
+
items: number[] = [];
|
|
3265
|
+
}
|
|
3266
|
+
`,
|
|
3267
|
+
expected: dedent`
|
|
3268
|
+
class Store {
|
|
3269
|
+
mut_items: string[] = [];
|
|
3270
|
+
readonly items: readonly number[] = [];
|
|
3271
|
+
}
|
|
3272
|
+
`,
|
|
3273
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3274
|
+
},
|
|
3275
|
+
{
|
|
3276
|
+
name: 'Method parameter with ignored prefix',
|
|
3277
|
+
source: dedent`
|
|
3278
|
+
class Store {
|
|
3279
|
+
process(mut_data: string[], data: number[]) {}
|
|
3280
|
+
}
|
|
3281
|
+
`,
|
|
3282
|
+
expected: dedent`
|
|
3283
|
+
class Store {
|
|
3284
|
+
process(mut_data: string[], data: readonly number[]) {}
|
|
3285
|
+
}
|
|
3286
|
+
`,
|
|
3287
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3288
|
+
},
|
|
3289
|
+
{
|
|
3290
|
+
name: 'Function expression with ignored prefix',
|
|
3291
|
+
source: dedent`
|
|
3292
|
+
const fn = function mut_process(data: string[]): number[] { return []; }
|
|
3293
|
+
`,
|
|
3294
|
+
expected: dedent`
|
|
3295
|
+
const fn = function mut_process(data: string[]): number[] { return []; }
|
|
3296
|
+
`,
|
|
3297
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3298
|
+
},
|
|
3299
|
+
{
|
|
3300
|
+
name: 'Array binding pattern in variable declaration (not implemented)',
|
|
3301
|
+
source: dedent`
|
|
3302
|
+
const [mut_first, mut_second]: [string[], number[]] = [[], []];
|
|
3303
|
+
`,
|
|
3304
|
+
expected: dedent`
|
|
3305
|
+
const [mut_first, mut_second]: readonly [readonly string[], readonly number[]] = [[], []];
|
|
3306
|
+
`,
|
|
3307
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3308
|
+
},
|
|
3309
|
+
{
|
|
3310
|
+
name: 'Object binding pattern in variable declaration (not implemented)',
|
|
3311
|
+
source: dedent`
|
|
3312
|
+
const { mut_arr, mut_obj }: { mut_arr: string[], mut_obj: number[] } = { mut_arr: [], mut_obj: [] };
|
|
3313
|
+
`,
|
|
3314
|
+
expected: dedent`
|
|
3315
|
+
const { mut_arr, mut_obj }: Readonly<{ mut_arr: string[], mut_obj: number[] }> = { mut_arr: [], mut_obj: [] };
|
|
3316
|
+
`,
|
|
3317
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
name: 'Parameter with ignored prefix in class constructor',
|
|
3321
|
+
source: dedent`
|
|
3322
|
+
class Example {
|
|
3323
|
+
constructor(mut_config: string[]) {}
|
|
3324
|
+
}
|
|
3325
|
+
`,
|
|
3326
|
+
expected: dedent`
|
|
3327
|
+
class Example {
|
|
3328
|
+
constructor(mut_config: string[]) {}
|
|
3329
|
+
}
|
|
3330
|
+
`,
|
|
3331
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3332
|
+
},
|
|
3333
|
+
{
|
|
3334
|
+
name: 'Parameter with ignored prefix in type literal method',
|
|
3335
|
+
source: dedent`
|
|
3336
|
+
type Handler = {
|
|
3337
|
+
process(mut_data: string[]): void;
|
|
3338
|
+
}
|
|
3339
|
+
`,
|
|
3340
|
+
expected: dedent`
|
|
3341
|
+
type Handler = Readonly<{
|
|
3342
|
+
process(mut_data: string[]): void;
|
|
3343
|
+
}>;
|
|
3344
|
+
`,
|
|
3345
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3346
|
+
},
|
|
3347
|
+
{
|
|
3348
|
+
name: 'Multiple parameters with ignored prefix',
|
|
3349
|
+
source: dedent`
|
|
3350
|
+
interface API {
|
|
3351
|
+
update(mut_id: string, mut_data: object[], data: string[]): void;
|
|
3352
|
+
}
|
|
3353
|
+
`,
|
|
3354
|
+
expected: dedent`
|
|
3355
|
+
interface API {
|
|
3356
|
+
update(mut_id: string, mut_data: object[], data: readonly string[]): void;
|
|
3357
|
+
}
|
|
3358
|
+
`,
|
|
3359
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3360
|
+
},
|
|
3361
|
+
{
|
|
3362
|
+
name: 'Arrow function with ignored prefix parameters',
|
|
3363
|
+
source: dedent`
|
|
3364
|
+
const handler = (mut_event: Event[], event: CustomEvent[]) => {};
|
|
3365
|
+
`,
|
|
3366
|
+
expected: dedent`
|
|
3367
|
+
const handler = (mut_event: Event[], event: readonly CustomEvent[]) => {};
|
|
3368
|
+
`,
|
|
3369
|
+
options: { ignorePrefixes: ['mut_'] },
|
|
3370
|
+
},
|
|
3371
|
+
])('$name', testFn);
|
|
3372
|
+
});
|
|
3373
|
+
|
|
3374
|
+
describe('Disable comment handling', () => {
|
|
3375
|
+
test.each([
|
|
3376
|
+
{
|
|
3377
|
+
name: 'Skip Array type with disable comment',
|
|
3378
|
+
source: dedent`
|
|
3379
|
+
// transformer-ignore-next-line
|
|
3380
|
+
type Foo = number[]; // Should not change to readonly number[]
|
|
3381
|
+
`,
|
|
3382
|
+
expected: dedent`
|
|
3383
|
+
// transformer-ignore-next-line
|
|
3384
|
+
type Foo = number[]; // Should not change to readonly number[]
|
|
3385
|
+
`,
|
|
3386
|
+
},
|
|
3387
|
+
{
|
|
3388
|
+
name: 'Skip Generic Array type with disable comment',
|
|
3389
|
+
source: dedent`
|
|
3390
|
+
// transformer-ignore-next-line
|
|
3391
|
+
type Bar = Array<string>; // Should not change to readonly string[]
|
|
3392
|
+
`,
|
|
3393
|
+
expected: dedent`
|
|
3394
|
+
// transformer-ignore-next-line
|
|
3395
|
+
type Bar = Array<string>; // Should not change to readonly string[]
|
|
3396
|
+
`,
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
name: 'Skip Tuple type with disable comment',
|
|
3400
|
+
source: dedent`
|
|
3401
|
+
// transformer-ignore-next-line
|
|
3402
|
+
type Baz = [number, string]; // Should not change to readonly [...]
|
|
3403
|
+
`,
|
|
3404
|
+
expected: dedent`
|
|
3405
|
+
// transformer-ignore-next-line
|
|
3406
|
+
type Baz = [number, string]; // Should not change to readonly [...]
|
|
3407
|
+
`,
|
|
3408
|
+
},
|
|
3409
|
+
{
|
|
3410
|
+
name: 'Skip Set type with disable comment',
|
|
3411
|
+
source: dedent`
|
|
3412
|
+
// transformer-ignore-next-line
|
|
3413
|
+
type Qux = Set<number>; // Should not change to ReadonlySet
|
|
3414
|
+
`,
|
|
3415
|
+
expected: dedent`
|
|
3416
|
+
// transformer-ignore-next-line
|
|
3417
|
+
type Qux = Set<number>; // Should not change to ReadonlySet
|
|
3418
|
+
`,
|
|
3419
|
+
},
|
|
3420
|
+
{
|
|
3421
|
+
name: 'Skip Map type with disable comment',
|
|
3422
|
+
source: dedent`
|
|
3423
|
+
// transformer-ignore-next-line
|
|
3424
|
+
type Quux = Map<string, boolean>; // Should not change to ReadonlyMap
|
|
3425
|
+
`,
|
|
3426
|
+
expected: dedent`
|
|
3427
|
+
// transformer-ignore-next-line
|
|
3428
|
+
type Quux = Map<string, boolean>; // Should not change to ReadonlyMap
|
|
3429
|
+
`,
|
|
3430
|
+
},
|
|
3431
|
+
{
|
|
3432
|
+
name: 'Skip Interface property with disable comment',
|
|
3433
|
+
source: dedent`
|
|
3434
|
+
interface MyInterface {
|
|
3435
|
+
// transformer-ignore-next-line
|
|
3436
|
+
prop: string[]; // This specific property should be skipped
|
|
3437
|
+
anotherProp: number[]; // This should still be transformed
|
|
3438
|
+
}
|
|
3439
|
+
`,
|
|
3440
|
+
expected: dedent`
|
|
3441
|
+
interface MyInterface {
|
|
3442
|
+
// transformer-ignore-next-line
|
|
3443
|
+
prop: string[]; // This specific property should be skipped
|
|
3444
|
+
readonly anotherProp: readonly number[]; // This should still be transformed
|
|
3445
|
+
}
|
|
3446
|
+
`,
|
|
3447
|
+
},
|
|
3448
|
+
{
|
|
3449
|
+
name: 'Skip whole Interface with disable comment',
|
|
3450
|
+
source: dedent`
|
|
3451
|
+
// transformer-ignore-next-line
|
|
3452
|
+
interface MyInterface {
|
|
3453
|
+
prop: string[]; // Should not change
|
|
3454
|
+
anotherProp: number[]; // Should not change
|
|
3455
|
+
}
|
|
3456
|
+
`,
|
|
3457
|
+
expected: dedent`
|
|
3458
|
+
// transformer-ignore-next-line
|
|
3459
|
+
interface MyInterface {
|
|
3460
|
+
prop: string[]; // Should not change
|
|
3461
|
+
anotherProp: number[]; // Should not change
|
|
3462
|
+
}
|
|
3463
|
+
`,
|
|
3464
|
+
},
|
|
3465
|
+
{
|
|
3466
|
+
name: 'Skip Class property with disable comment',
|
|
3467
|
+
source: dedent`
|
|
3468
|
+
class MyClass {
|
|
3469
|
+
// transformer-ignore-next-line
|
|
3470
|
+
prop: number[] = []; // This specific property should be skipped
|
|
3471
|
+
another: boolean[] = []; // This should still be transformed
|
|
3472
|
+
}
|
|
3473
|
+
`,
|
|
3474
|
+
expected: dedent`
|
|
3475
|
+
class MyClass {
|
|
3476
|
+
// transformer-ignore-next-line
|
|
3477
|
+
prop: number[] = []; // This specific property should be skipped
|
|
3478
|
+
readonly another: readonly boolean[] = []; // This should still be transformed
|
|
3479
|
+
}
|
|
3480
|
+
`,
|
|
3481
|
+
},
|
|
3482
|
+
{
|
|
3483
|
+
name: 'Skip whole Class with disable comment',
|
|
3484
|
+
source: dedent`
|
|
3485
|
+
// transformer-ignore-next-line
|
|
3486
|
+
class MyClass {
|
|
3487
|
+
prop: number[] = []; // Should not change
|
|
3488
|
+
another: boolean[] = []; // Should not change
|
|
3489
|
+
}
|
|
3490
|
+
`,
|
|
3491
|
+
expected: dedent`
|
|
3492
|
+
// transformer-ignore-next-line
|
|
3493
|
+
class MyClass {
|
|
3494
|
+
prop: number[] = []; // Should not change
|
|
3495
|
+
another: boolean[] = []; // Should not change
|
|
3496
|
+
}
|
|
3497
|
+
`,
|
|
3498
|
+
},
|
|
3499
|
+
{
|
|
3500
|
+
name: 'Skip Function parameter type with disable comment',
|
|
3501
|
+
source: dedent`
|
|
3502
|
+
function myFunc(
|
|
3503
|
+
a: string[], // Should be transformed
|
|
3504
|
+
// transformer-ignore-next-line
|
|
3505
|
+
b: Map<string, number>, // Should be skipped
|
|
3506
|
+
c: Set<boolean> // Should be transformed
|
|
3507
|
+
) {}
|
|
3508
|
+
`,
|
|
3509
|
+
expected: dedent`
|
|
3510
|
+
function myFunc(
|
|
3511
|
+
a: readonly string[], // Should be transformed
|
|
3512
|
+
// transformer-ignore-next-line
|
|
3513
|
+
b: Map<string, number>, // Should be skipped
|
|
3514
|
+
c: ReadonlySet<boolean> // Should be transformed
|
|
3515
|
+
) {}
|
|
3516
|
+
`,
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
name: 'Skip whole Function declaration with disable comment',
|
|
3520
|
+
source: dedent`
|
|
3521
|
+
// transformer-ignore-next-line
|
|
3522
|
+
function myFunc(a: string[], b: Map<string, number>): Set<boolean>[] {} // Should not change
|
|
3523
|
+
`,
|
|
3524
|
+
expected: dedent`
|
|
3525
|
+
// transformer-ignore-next-line
|
|
3526
|
+
function myFunc(a: string[], b: Map<string, number>): Set<boolean>[] {} // Should not change
|
|
3527
|
+
`,
|
|
3528
|
+
},
|
|
3529
|
+
{
|
|
3530
|
+
name: 'Skip Function return type with disable comment',
|
|
3531
|
+
source: dedent`
|
|
3532
|
+
function myFunc(
|
|
3533
|
+
a: string[] // Param should be transformed
|
|
3534
|
+
):
|
|
3535
|
+
// transformer-ignore-next-line
|
|
3536
|
+
Set<number>[] {} // Return type should be skipped
|
|
3537
|
+
`,
|
|
3538
|
+
expected: dedent`
|
|
3539
|
+
function myFunc(
|
|
3540
|
+
a: readonly string[] // Param should be transformed
|
|
3541
|
+
):
|
|
3542
|
+
// transformer-ignore-next-line
|
|
3543
|
+
Set<number>[] {} // Return type should be skipped
|
|
3544
|
+
`,
|
|
3545
|
+
},
|
|
3546
|
+
{
|
|
3547
|
+
name: 'Skip Type literal with disable comment',
|
|
3548
|
+
source: dedent`
|
|
3549
|
+
// transformer-ignore-next-line
|
|
3550
|
+
type Ignored = { x: number[] }; // Should not change to Readonly<{...}>
|
|
3551
|
+
`,
|
|
3552
|
+
expected: dedent`
|
|
3553
|
+
// transformer-ignore-next-line
|
|
3554
|
+
type Ignored = { x: number[] }; // Should not change to Readonly<{...}>
|
|
3555
|
+
`,
|
|
3556
|
+
},
|
|
3557
|
+
{
|
|
3558
|
+
name: 'Disable comment only affects next line (mixed constructs)',
|
|
3559
|
+
source: dedent`
|
|
3560
|
+
type A = number[]; // Should be transformed
|
|
3561
|
+
// transformer-ignore-next-line
|
|
3562
|
+
type B = string[]; // Should be skipped
|
|
3563
|
+
type C = boolean[]; // Should be transformed
|
|
3564
|
+
`,
|
|
3565
|
+
expected: dedent`
|
|
3566
|
+
type A = readonly number[]; // Should be transformed
|
|
3567
|
+
// transformer-ignore-next-line
|
|
3568
|
+
type B = string[]; // Should be skipped
|
|
3569
|
+
type C = readonly boolean[]; // Should be transformed
|
|
3570
|
+
`,
|
|
3571
|
+
},
|
|
3572
|
+
{
|
|
3573
|
+
name: 'File scope transformer-ignore',
|
|
3574
|
+
source: dedent`
|
|
3575
|
+
/* transformer-ignore */
|
|
3576
|
+
type A = number[]; // Should be skipped
|
|
3577
|
+
type B = string[]; // Should be skipped
|
|
3578
|
+
type C = boolean[]; // Should be skipped
|
|
3579
|
+
`,
|
|
3580
|
+
expected: dedent`
|
|
3581
|
+
/* transformer-ignore */
|
|
3582
|
+
type A = number[]; // Should be skipped
|
|
3583
|
+
type B = string[]; // Should be skipped
|
|
3584
|
+
type C = boolean[]; // Should be skipped
|
|
3585
|
+
`,
|
|
3586
|
+
},
|
|
3587
|
+
])('$name', testFn);
|
|
3588
|
+
});
|
|
3589
|
+
|
|
3590
|
+
describe('Error Cases', () => {
|
|
3591
|
+
test('Invalid DeepReadonlyTypeName', () => {
|
|
3592
|
+
expect(() => {
|
|
3593
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
3594
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
3595
|
+
|
|
3596
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
3597
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
3598
|
+
|
|
3599
|
+
const source = dedent`
|
|
3600
|
+
type T = number[];
|
|
3601
|
+
`;
|
|
3602
|
+
|
|
3603
|
+
transformSourceCode(source, false, [
|
|
3604
|
+
convertToReadonlyTypeTransformer({
|
|
3605
|
+
DeepReadonly: {
|
|
3606
|
+
typeName: 'Readonly',
|
|
3607
|
+
},
|
|
3608
|
+
}),
|
|
3609
|
+
]);
|
|
3610
|
+
}).toThrowError(
|
|
3611
|
+
'Invalid DeepReadonly typeName "Readonly" passed to convertToReadonlyType',
|
|
3612
|
+
);
|
|
3613
|
+
});
|
|
3614
|
+
|
|
3615
|
+
test('Unexpected number of type arguments for Array', () => {
|
|
3616
|
+
expect(() => {
|
|
3617
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
3618
|
+
vi.spyOn(console, 'debug').mockImplementation(() => {});
|
|
3619
|
+
|
|
3620
|
+
// eslint-disable-next-line vitest/no-restricted-vi-methods
|
|
3621
|
+
vi.spyOn(console, 'log').mockImplementation(() => {});
|
|
3622
|
+
|
|
3623
|
+
const source = dedent`
|
|
3624
|
+
type T = Array<number, string>;
|
|
3625
|
+
`; // Invalid Array usage
|
|
3626
|
+
|
|
3627
|
+
transformSourceCode(source, false, [
|
|
3628
|
+
convertToReadonlyTypeTransformer(),
|
|
3629
|
+
]);
|
|
3630
|
+
}).toThrowError('Unexpected number of type arguments');
|
|
3631
|
+
});
|
|
3632
|
+
});
|
|
3633
|
+
});
|
|
3634
|
+
|
|
3635
|
+
const toUnionAndIntersectionTestCase = ({
|
|
3636
|
+
title,
|
|
3637
|
+
testCase,
|
|
3638
|
+
}: Readonly<{
|
|
3639
|
+
title: (op: 'Union' | 'Intersection') => string;
|
|
3640
|
+
testCase: (op: '|' | '&') => Readonly<{
|
|
3641
|
+
source: string;
|
|
3642
|
+
expected: string;
|
|
3643
|
+
debug?: boolean;
|
|
3644
|
+
}>;
|
|
3645
|
+
}>): readonly Readonly<{
|
|
3646
|
+
name: string;
|
|
3647
|
+
source: string;
|
|
3648
|
+
expected: string;
|
|
3649
|
+
}>[] =>
|
|
3650
|
+
(['|', '&'] as const).map((op) => ({
|
|
3651
|
+
name: title(op === '&' ? 'Intersection' : 'Union'),
|
|
3652
|
+
...testCase(op),
|
|
3653
|
+
}));
|