@tsonic/frontend 0.0.11 → 0.0.13

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.
Files changed (125) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/ir/converters/expressions/access.d.ts.map +1 -1
  3. package/dist/ir/converters/expressions/access.js +61 -1
  4. package/dist/ir/converters/expressions/access.js.map +1 -1
  5. package/dist/ir/converters/expressions/calls.d.ts.map +1 -1
  6. package/dist/ir/converters/expressions/calls.js +293 -24
  7. package/dist/ir/converters/expressions/calls.js.map +1 -1
  8. package/dist/ir/converters/expressions/helpers.js +4 -4
  9. package/dist/ir/converters/expressions/helpers.js.map +1 -1
  10. package/dist/ir/converters/expressions/literals.d.ts +14 -0
  11. package/dist/ir/converters/expressions/literals.d.ts.map +1 -1
  12. package/dist/ir/converters/expressions/literals.js +22 -2
  13. package/dist/ir/converters/expressions/literals.js.map +1 -1
  14. package/dist/ir/converters/expressions/numeric-recovery.test.js +3 -2
  15. package/dist/ir/converters/expressions/numeric-recovery.test.js.map +1 -1
  16. package/dist/ir/converters/statements/helpers.d.ts.map +1 -1
  17. package/dist/ir/converters/statements/helpers.js +10 -4
  18. package/dist/ir/converters/statements/helpers.js.map +1 -1
  19. package/dist/ir/expression-converter.d.ts.map +1 -1
  20. package/dist/ir/expression-converter.js +38 -5
  21. package/dist/ir/expression-converter.js.map +1 -1
  22. package/dist/ir/index.d.ts +1 -1
  23. package/dist/ir/index.d.ts.map +1 -1
  24. package/dist/ir/index.js +1 -1
  25. package/dist/ir/index.js.map +1 -1
  26. package/dist/ir/statement-converter.d.ts.map +1 -1
  27. package/dist/ir/statement-converter.js +12 -0
  28. package/dist/ir/statement-converter.js.map +1 -1
  29. package/dist/ir/type-converter/inference.d.ts.map +1 -1
  30. package/dist/ir/type-converter/inference.js +50 -7
  31. package/dist/ir/type-converter/inference.js.map +1 -1
  32. package/dist/ir/type-converter/primitives.d.ts +26 -4
  33. package/dist/ir/type-converter/primitives.d.ts.map +1 -1
  34. package/dist/ir/type-converter/primitives.js +36 -2
  35. package/dist/ir/type-converter/primitives.js.map +1 -1
  36. package/dist/ir/type-converter/references.d.ts.map +1 -1
  37. package/dist/ir/type-converter/references.js +324 -11
  38. package/dist/ir/type-converter/references.js.map +1 -1
  39. package/dist/ir/type-converter/utility-types.d.ts +93 -0
  40. package/dist/ir/type-converter/utility-types.d.ts.map +1 -0
  41. package/dist/ir/type-converter/utility-types.js +528 -0
  42. package/dist/ir/type-converter/utility-types.js.map +1 -0
  43. package/dist/ir/type-converter/utility-types.test.d.ts +10 -0
  44. package/dist/ir/type-converter/utility-types.test.d.ts.map +1 -0
  45. package/dist/ir/type-converter/utility-types.test.js +1030 -0
  46. package/dist/ir/type-converter/utility-types.test.js.map +1 -0
  47. package/dist/ir/types/expressions.d.ts +40 -2
  48. package/dist/ir/types/expressions.d.ts.map +1 -1
  49. package/dist/ir/types/helpers.d.ts +3 -1
  50. package/dist/ir/types/helpers.d.ts.map +1 -1
  51. package/dist/ir/types/index.d.ts +2 -1
  52. package/dist/ir/types/index.d.ts.map +1 -1
  53. package/dist/ir/types/index.js +2 -0
  54. package/dist/ir/types/index.js.map +1 -1
  55. package/dist/ir/types/ir-types.d.ts +69 -11
  56. package/dist/ir/types/ir-types.d.ts.map +1 -1
  57. package/dist/ir/types/numeric-helpers.d.ts +46 -0
  58. package/dist/ir/types/numeric-helpers.d.ts.map +1 -0
  59. package/dist/ir/types/numeric-helpers.js +105 -0
  60. package/dist/ir/types/numeric-helpers.js.map +1 -0
  61. package/dist/ir/types/statements.d.ts +11 -1
  62. package/dist/ir/types/statements.d.ts.map +1 -1
  63. package/dist/ir/types.d.ts +2 -2
  64. package/dist/ir/types.d.ts.map +1 -1
  65. package/dist/ir/types.js +3 -1
  66. package/dist/ir/types.js.map +1 -1
  67. package/dist/ir/validation/anonymous-type-lowering-pass.d.ts +32 -0
  68. package/dist/ir/validation/anonymous-type-lowering-pass.d.ts.map +1 -0
  69. package/dist/ir/validation/anonymous-type-lowering-pass.js +854 -0
  70. package/dist/ir/validation/anonymous-type-lowering-pass.js.map +1 -0
  71. package/dist/ir/validation/attribute-collection-pass.d.ts +37 -0
  72. package/dist/ir/validation/attribute-collection-pass.d.ts.map +1 -0
  73. package/dist/ir/validation/attribute-collection-pass.js +282 -0
  74. package/dist/ir/validation/attribute-collection-pass.js.map +1 -0
  75. package/dist/ir/validation/attribute-collection-pass.test.d.ts +5 -0
  76. package/dist/ir/validation/attribute-collection-pass.test.d.ts.map +1 -0
  77. package/dist/ir/validation/attribute-collection-pass.test.js +215 -0
  78. package/dist/ir/validation/attribute-collection-pass.test.js.map +1 -0
  79. package/dist/ir/validation/index.d.ts +3 -0
  80. package/dist/ir/validation/index.d.ts.map +1 -1
  81. package/dist/ir/validation/index.js +3 -0
  82. package/dist/ir/validation/index.js.map +1 -1
  83. package/dist/ir/validation/numeric-coercion-pass.d.ts +77 -0
  84. package/dist/ir/validation/numeric-coercion-pass.d.ts.map +1 -0
  85. package/dist/ir/validation/numeric-coercion-pass.js +686 -0
  86. package/dist/ir/validation/numeric-coercion-pass.js.map +1 -0
  87. package/dist/ir/validation/numeric-invariants.test.js +130 -14
  88. package/dist/ir/validation/numeric-invariants.test.js.map +1 -1
  89. package/dist/ir/validation/numeric-proof-pass.d.ts.map +1 -1
  90. package/dist/ir/validation/numeric-proof-pass.js +68 -108
  91. package/dist/ir/validation/numeric-proof-pass.js.map +1 -1
  92. package/dist/ir/validation/soundness-gate.d.ts.map +1 -1
  93. package/dist/ir/validation/soundness-gate.js +23 -12
  94. package/dist/ir/validation/soundness-gate.js.map +1 -1
  95. package/dist/ir/validation/yield-lowering-pass.test.js +21 -12
  96. package/dist/ir/validation/yield-lowering-pass.test.js.map +1 -1
  97. package/dist/program/bindings.d.ts +20 -9
  98. package/dist/program/bindings.d.ts.map +1 -1
  99. package/dist/program/bindings.js +98 -36
  100. package/dist/program/bindings.js.map +1 -1
  101. package/dist/program/bindings.test.js +3 -3
  102. package/dist/program/dependency-graph.d.ts.map +1 -1
  103. package/dist/program/dependency-graph.js +31 -5
  104. package/dist/program/dependency-graph.js.map +1 -1
  105. package/dist/resolver/import-resolution.d.ts +4 -0
  106. package/dist/resolver/import-resolution.d.ts.map +1 -1
  107. package/dist/resolver/import-resolution.js +18 -7
  108. package/dist/resolver/import-resolution.js.map +1 -1
  109. package/dist/resolver.test.js +2 -2
  110. package/dist/resolver.test.js.map +1 -1
  111. package/dist/types/diagnostic.d.ts +1 -1
  112. package/dist/types/diagnostic.d.ts.map +1 -1
  113. package/dist/types/diagnostic.js.map +1 -1
  114. package/dist/validation/generics.d.ts.map +1 -1
  115. package/dist/validation/generics.js +133 -1
  116. package/dist/validation/generics.js.map +1 -1
  117. package/dist/validation/static-safety.js +13 -0
  118. package/dist/validation/static-safety.js.map +1 -1
  119. package/dist/validation/unsupported-utility-types.d.ts +10 -8
  120. package/dist/validation/unsupported-utility-types.d.ts.map +1 -1
  121. package/dist/validation/unsupported-utility-types.js +12 -19
  122. package/dist/validation/unsupported-utility-types.js.map +1 -1
  123. package/dist/validator.test.js +133 -28
  124. package/dist/validator.test.js.map +1 -1
  125. package/package.json +1 -1
@@ -0,0 +1,1030 @@
1
+ /**
2
+ * Tests for utility type expansion
3
+ *
4
+ * Covers the safety guarantees per Alice's review:
5
+ * 1. Index signatures block expansion (never drop members)
6
+ * 2. Symbol/computed keys block expansion (never drop members)
7
+ * 3. Explicit undefined is preserved (not stripped)
8
+ */
9
+ import { describe, it } from "mocha";
10
+ import { expect } from "chai";
11
+ import * as ts from "typescript";
12
+ import { expandUtilityType, expandConditionalUtilityType, expandRecordType, } from "./utility-types.js";
13
+ /**
14
+ * Assert value is not null/undefined and return it typed as non-null.
15
+ * Throws if value is null or undefined.
16
+ */
17
+ function assertDefined(value, msg) {
18
+ if (value === null || value === undefined) {
19
+ throw new Error(msg ?? "Expected value to be defined");
20
+ }
21
+ return value;
22
+ }
23
+ /**
24
+ * Helper to create a TypeScript program from source code
25
+ */
26
+ const createTestProgram = (source, fileName = "test.ts") => {
27
+ const compilerOptions = {
28
+ target: ts.ScriptTarget.ES2022,
29
+ module: ts.ModuleKind.NodeNext,
30
+ strict: true,
31
+ noEmit: true,
32
+ };
33
+ const host = ts.createCompilerHost(compilerOptions);
34
+ const originalGetSourceFile = host.getSourceFile;
35
+ const originalFileExists = host.fileExists;
36
+ const originalReadFile = host.readFile;
37
+ host.getSourceFile = (name, languageVersionOrOptions, onError, shouldCreateNewSourceFile) => {
38
+ if (name === fileName) {
39
+ return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
40
+ }
41
+ return originalGetSourceFile.call(host, name, languageVersionOrOptions, onError, shouldCreateNewSourceFile);
42
+ };
43
+ host.fileExists = (name) => name === fileName || originalFileExists.call(host, name);
44
+ host.readFile = (name) => name === fileName ? source : originalReadFile.call(host, name);
45
+ const program = ts.createProgram([fileName], compilerOptions, host);
46
+ const sourceFile = assertDefined(program.getSourceFile(fileName), `Source file ${fileName} not found`);
47
+ const checker = program.getTypeChecker();
48
+ return { program, checker, sourceFile };
49
+ };
50
+ /**
51
+ * Helper to find a type alias by name and get its type reference node
52
+ */
53
+ const findTypeAliasReference = (sourceFile, aliasName) => {
54
+ let result = null;
55
+ const visitor = (node) => {
56
+ if (ts.isTypeAliasDeclaration(node) && node.name.text === aliasName) {
57
+ if (ts.isTypeReferenceNode(node.type)) {
58
+ result = node.type;
59
+ }
60
+ }
61
+ ts.forEachChild(node, visitor);
62
+ };
63
+ ts.forEachChild(sourceFile, visitor);
64
+ return result;
65
+ };
66
+ /**
67
+ * Stub convertType for testing - just returns the type name
68
+ */
69
+ const stubConvertType = (node, checker) => {
70
+ if (ts.isTypeReferenceNode(node)) {
71
+ const name = ts.isIdentifier(node.typeName)
72
+ ? node.typeName.text
73
+ : node.typeName.getText();
74
+ return { kind: "referenceType", name, typeArguments: [] };
75
+ }
76
+ if (node.kind === ts.SyntaxKind.StringKeyword) {
77
+ return { kind: "primitiveType", name: "string" };
78
+ }
79
+ if (node.kind === ts.SyntaxKind.NumberKeyword) {
80
+ return { kind: "primitiveType", name: "number" };
81
+ }
82
+ if (node.kind === ts.SyntaxKind.UndefinedKeyword) {
83
+ return { kind: "primitiveType", name: "undefined" };
84
+ }
85
+ if (node.kind === ts.SyntaxKind.NeverKeyword) {
86
+ return { kind: "neverType" };
87
+ }
88
+ // Handle literal type nodes (e.g., "a", 1)
89
+ if (ts.isLiteralTypeNode(node)) {
90
+ const literal = node.literal;
91
+ if (ts.isStringLiteral(literal)) {
92
+ return { kind: "literalType", value: literal.text };
93
+ }
94
+ if (ts.isNumericLiteral(literal)) {
95
+ return { kind: "literalType", value: Number(literal.text) };
96
+ }
97
+ }
98
+ if (ts.isUnionTypeNode(node)) {
99
+ return {
100
+ kind: "unionType",
101
+ types: node.types.map((t) => stubConvertType(t, checker)),
102
+ };
103
+ }
104
+ return { kind: "anyType" };
105
+ };
106
+ describe("Utility Type Expansion Safety", () => {
107
+ describe("Index signatures block expansion", () => {
108
+ it("should return null for Partial<T> when T has string index signature", () => {
109
+ const source = `
110
+ interface WithStringIndex {
111
+ [key: string]: number;
112
+ name: string;
113
+ }
114
+ type PartialWithIndex = Partial<WithStringIndex>;
115
+ `;
116
+ const { checker, sourceFile } = createTestProgram(source);
117
+ const typeRef = findTypeAliasReference(sourceFile, "PartialWithIndex");
118
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
119
+ // Should return null because expansion would lose the index signature
120
+ expect(result).to.equal(null);
121
+ });
122
+ it("should return null for Readonly<T> when T has number index signature", () => {
123
+ const source = `
124
+ interface WithNumberIndex {
125
+ [key: number]: string;
126
+ length: number;
127
+ }
128
+ type ReadonlyWithIndex = Readonly<WithNumberIndex>;
129
+ `;
130
+ const { checker, sourceFile } = createTestProgram(source);
131
+ const typeRef = findTypeAliasReference(sourceFile, "ReadonlyWithIndex");
132
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Readonly", checker, stubConvertType);
133
+ // Should return null because expansion would lose the index signature
134
+ expect(result).to.equal(null);
135
+ });
136
+ it("should expand normally when T has no index signatures", () => {
137
+ const source = `
138
+ interface Person {
139
+ name: string;
140
+ age: number;
141
+ }
142
+ type PartialPerson = Partial<Person>;
143
+ `;
144
+ const { checker, sourceFile } = createTestProgram(source);
145
+ const typeRef = findTypeAliasReference(sourceFile, "PartialPerson");
146
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
147
+ // Should expand successfully
148
+ expect(result).not.to.equal(null);
149
+ expect(result?.kind).to.equal("objectType");
150
+ expect(result?.members).to.have.length(2);
151
+ });
152
+ });
153
+ describe("Symbol/computed keys block expansion", () => {
154
+ it("should return null when T has symbol keys", () => {
155
+ // Note: Symbol keys in TypeScript are represented with __@ prefix internally
156
+ // This test validates that the expansion correctly identifies and rejects them
157
+ const source = `
158
+ const sym = Symbol("test");
159
+ interface WithSymbol {
160
+ [sym]: string;
161
+ name: string;
162
+ }
163
+ type PartialWithSymbol = Partial<WithSymbol>;
164
+ `;
165
+ const { checker, sourceFile } = createTestProgram(source);
166
+ const typeRef = findTypeAliasReference(sourceFile, "PartialWithSymbol");
167
+ // This may or may not find the type ref depending on how TS handles it
168
+ if (typeRef) {
169
+ const result = expandUtilityType(typeRef, "Partial", checker, stubConvertType);
170
+ // If expansion proceeds, it should return null due to symbol key
171
+ // (symbol keys start with __@ internally)
172
+ // Note: The actual behavior depends on whether TS resolves the symbol key
173
+ // Either result is null (rejected) or expanded (symbol was ignored)
174
+ expect(result === null || result.kind === "objectType").to.equal(true);
175
+ }
176
+ // Test passes if we get here - the key insight is the code handles this case
177
+ });
178
+ });
179
+ describe("Explicit undefined preservation", () => {
180
+ it("should preserve explicit undefined in optional property type", () => {
181
+ const source = `
182
+ interface WithExplicitUndefined {
183
+ x?: string | undefined;
184
+ }
185
+ type PartialWithUndefined = Partial<WithExplicitUndefined>;
186
+ `;
187
+ const { checker, sourceFile } = createTestProgram(source);
188
+ const typeRef = findTypeAliasReference(sourceFile, "PartialWithUndefined");
189
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
190
+ expect(result).not.to.equal(null);
191
+ expect(result?.kind).to.equal("objectType");
192
+ // The property should preserve the union with undefined
193
+ const xProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "x");
194
+ expect(xProp).not.to.equal(undefined);
195
+ // The type should be a union containing undefined
196
+ if (xProp && xProp.kind === "propertySignature") {
197
+ // With explicit undefined, the type should include undefined in the union
198
+ // The exact representation depends on whether we stripped synthetic undefined
199
+ // The key is that we DON'T strip it when explicit undefined was declared
200
+ expect(xProp.type.kind).to.equal("unionType");
201
+ }
202
+ });
203
+ it("should strip synthetic undefined from optional property without explicit undefined", () => {
204
+ const source = `
205
+ interface WithSyntheticUndefined {
206
+ x?: string;
207
+ }
208
+ type PartialWithSynthetic = Partial<WithSyntheticUndefined>;
209
+ `;
210
+ const { checker, sourceFile } = createTestProgram(source);
211
+ const typeRef = findTypeAliasReference(sourceFile, "PartialWithSynthetic");
212
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
213
+ expect(result).not.to.equal(null);
214
+ expect(result?.kind).to.equal("objectType");
215
+ // The property type should be string (not string | undefined)
216
+ // because we strip synthetic undefined
217
+ const xProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "x");
218
+ expect(xProp).not.to.equal(undefined);
219
+ if (xProp && xProp.kind === "propertySignature") {
220
+ // Synthetic undefined should be stripped, leaving just string
221
+ expect(xProp.type.kind).to.equal("primitiveType");
222
+ if (xProp.type.kind === "primitiveType") {
223
+ expect(xProp.type.name).to.equal("string");
224
+ }
225
+ }
226
+ });
227
+ it("should preserve explicit undefined in required property", () => {
228
+ const source = `
229
+ interface WithRequiredUndefined {
230
+ x: string | undefined;
231
+ }
232
+ type PartialRequired = Partial<WithRequiredUndefined>;
233
+ `;
234
+ const { checker, sourceFile } = createTestProgram(source);
235
+ const typeRef = findTypeAliasReference(sourceFile, "PartialRequired");
236
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
237
+ expect(result).not.to.equal(null);
238
+ const xProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "x");
239
+ expect(xProp).not.to.equal(undefined);
240
+ // Required property with explicit undefined should keep the union
241
+ if (xProp && xProp.kind === "propertySignature") {
242
+ expect(xProp.type.kind).to.equal("unionType");
243
+ }
244
+ });
245
+ });
246
+ describe("Readonly preservation in nested utility types", () => {
247
+ it("should preserve readonly in Partial<Readonly<T>>", () => {
248
+ const source = `
249
+ interface Person {
250
+ name: string;
251
+ age: number;
252
+ }
253
+ type PartialReadonly = Partial<Readonly<Person>>;
254
+ `;
255
+ const { checker, sourceFile } = createTestProgram(source);
256
+ const typeRef = findTypeAliasReference(sourceFile, "PartialReadonly");
257
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
258
+ expect(result).not.to.equal(null);
259
+ expect(result?.kind).to.equal("objectType");
260
+ // All properties should be readonly (from inner Readonly<T>)
261
+ const nameProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "name");
262
+ expect(nameProp).not.to.equal(undefined);
263
+ if (nameProp && nameProp.kind === "propertySignature") {
264
+ expect(nameProp.isReadonly).to.equal(true);
265
+ expect(nameProp.isOptional).to.equal(true);
266
+ }
267
+ });
268
+ it("should preserve readonly and optional in Readonly<Partial<T>>", () => {
269
+ const source = `
270
+ interface Person {
271
+ name: string;
272
+ age: number;
273
+ }
274
+ type ReadonlyPartial = Readonly<Partial<Person>>;
275
+ `;
276
+ const { checker, sourceFile } = createTestProgram(source);
277
+ const typeRef = findTypeAliasReference(sourceFile, "ReadonlyPartial");
278
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Readonly", checker, stubConvertType);
279
+ expect(result).not.to.equal(null);
280
+ expect(result?.kind).to.equal("objectType");
281
+ // All properties should be both readonly (from Readonly<T>) and optional (from Partial<T>)
282
+ const nameProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "name");
283
+ expect(nameProp).not.to.equal(undefined);
284
+ if (nameProp && nameProp.kind === "propertySignature") {
285
+ expect(nameProp.isReadonly).to.equal(true);
286
+ expect(nameProp.isOptional).to.equal(true);
287
+ }
288
+ });
289
+ });
290
+ describe("Method signatures in utility types", () => {
291
+ it("should expand interface with method as methodSignature", () => {
292
+ const source = `
293
+ interface WithMethod {
294
+ name: string;
295
+ greet(greeting: string): string;
296
+ }
297
+ type PartialWithMethod = Partial<WithMethod>;
298
+ `;
299
+ const { checker, sourceFile } = createTestProgram(source);
300
+ const typeRef = findTypeAliasReference(sourceFile, "PartialWithMethod");
301
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
302
+ expect(result).not.to.equal(null);
303
+ expect(result?.kind).to.equal("objectType");
304
+ // Should have both property and method
305
+ const nameProp = result?.members.find((m) => m.kind === "propertySignature" && m.name === "name");
306
+ const greetMethod = result?.members.find((m) => m.kind === "methodSignature" && m.name === "greet");
307
+ expect(nameProp).not.to.equal(undefined);
308
+ expect(greetMethod).not.to.equal(undefined);
309
+ // Method should have parameters
310
+ if (greetMethod && greetMethod.kind === "methodSignature") {
311
+ expect(greetMethod.parameters).to.have.length(1);
312
+ }
313
+ });
314
+ });
315
+ describe("Pick and Omit with multiple keys", () => {
316
+ it("should expand Pick with multiple keys", () => {
317
+ const source = `
318
+ interface Person {
319
+ name: string;
320
+ age: number;
321
+ email: string;
322
+ phone: string;
323
+ }
324
+ type ContactInfo = Pick<Person, "name" | "email" | "phone">;
325
+ `;
326
+ const { checker, sourceFile } = createTestProgram(source);
327
+ const typeRef = findTypeAliasReference(sourceFile, "ContactInfo");
328
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Pick", checker, stubConvertType);
329
+ expect(result).not.to.equal(null);
330
+ expect(result?.kind).to.equal("objectType");
331
+ // Should only have name, email, phone (not age)
332
+ expect(result?.members).to.have.length(3);
333
+ const propNames = result?.members
334
+ .filter((m) => m.kind === "propertySignature")
335
+ .map((m) => m.name);
336
+ expect(propNames).to.include("name");
337
+ expect(propNames).to.include("email");
338
+ expect(propNames).to.include("phone");
339
+ expect(propNames).not.to.include("age");
340
+ });
341
+ it("should expand Omit with multiple keys", () => {
342
+ const source = `
343
+ interface Person {
344
+ name: string;
345
+ age: number;
346
+ email: string;
347
+ phone: string;
348
+ }
349
+ type MinimalPerson = Omit<Person, "email" | "phone">;
350
+ `;
351
+ const { checker, sourceFile } = createTestProgram(source);
352
+ const typeRef = findTypeAliasReference(sourceFile, "MinimalPerson");
353
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Omit", checker, stubConvertType);
354
+ expect(result).not.to.equal(null);
355
+ expect(result?.kind).to.equal("objectType");
356
+ // Should only have name, age (not email, phone)
357
+ expect(result?.members).to.have.length(2);
358
+ const propNames = result?.members
359
+ .filter((m) => m.kind === "propertySignature")
360
+ .map((m) => m.name);
361
+ expect(propNames).to.include("name");
362
+ expect(propNames).to.include("age");
363
+ expect(propNames).not.to.include("email");
364
+ expect(propNames).not.to.include("phone");
365
+ });
366
+ });
367
+ describe("Type parameter detection", () => {
368
+ it("should return null for Partial<T> where T is a type parameter", () => {
369
+ const source = `
370
+ function process<T>(data: Partial<T>): void {}
371
+ `;
372
+ const { checker, sourceFile } = createTestProgram(source);
373
+ // Find the Partial<T> type reference in the function parameter
374
+ let typeRef = null;
375
+ const visitor = (node) => {
376
+ if (ts.isTypeReferenceNode(node) &&
377
+ ts.isIdentifier(node.typeName) &&
378
+ node.typeName.text === "Partial") {
379
+ typeRef = node;
380
+ }
381
+ ts.forEachChild(node, visitor);
382
+ };
383
+ ts.forEachChild(sourceFile, visitor);
384
+ const result = expandUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Partial", checker, stubConvertType);
385
+ // Should return null because T is a type parameter - can't expand at compile time
386
+ expect(result).to.equal(null);
387
+ });
388
+ });
389
+ });
390
+ describe("Conditional Utility Type Expansion", () => {
391
+ describe("NonNullable<T>", () => {
392
+ it("should expand NonNullable<string | null> to string", () => {
393
+ const source = `
394
+ type Result = NonNullable<string | null>;
395
+ `;
396
+ const { checker, sourceFile } = createTestProgram(source);
397
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
398
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "NonNullable", checker, stubConvertType);
399
+ expect(result).not.to.equal(null);
400
+ expect(result?.kind).to.equal("primitiveType");
401
+ if (result?.kind === "primitiveType") {
402
+ expect(result.name).to.equal("string");
403
+ }
404
+ });
405
+ it("should expand NonNullable<string | null | undefined> to string", () => {
406
+ const source = `
407
+ type Result = NonNullable<string | null | undefined>;
408
+ `;
409
+ const { checker, sourceFile } = createTestProgram(source);
410
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
411
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "NonNullable", checker, stubConvertType);
412
+ expect(result).not.to.equal(null);
413
+ expect(result?.kind).to.equal("primitiveType");
414
+ if (result?.kind === "primitiveType") {
415
+ expect(result.name).to.equal("string");
416
+ }
417
+ });
418
+ it("should return never for NonNullable<null | undefined>", () => {
419
+ const source = `
420
+ type Result = NonNullable<null | undefined>;
421
+ `;
422
+ const { checker, sourceFile } = createTestProgram(source);
423
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
424
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "NonNullable", checker, stubConvertType);
425
+ expect(result).not.to.equal(null);
426
+ expect(result?.kind).to.equal("neverType");
427
+ });
428
+ it("should preserve any for NonNullable<any>", () => {
429
+ const source = `
430
+ type Result = NonNullable<any>;
431
+ `;
432
+ const { checker, sourceFile } = createTestProgram(source);
433
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
434
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "NonNullable", checker, stubConvertType);
435
+ expect(result).not.to.equal(null);
436
+ expect(result?.kind).to.equal("anyType");
437
+ });
438
+ it("should preserve unknown for NonNullable<unknown>", () => {
439
+ const source = `
440
+ type Result = NonNullable<unknown>;
441
+ `;
442
+ const { checker, sourceFile } = createTestProgram(source);
443
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
444
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "NonNullable", checker, stubConvertType);
445
+ expect(result).not.to.equal(null);
446
+ expect(result?.kind).to.equal("unknownType");
447
+ });
448
+ it("should return null for NonNullable<T> where T is a type parameter", () => {
449
+ const source = `
450
+ function process<T>(data: NonNullable<T>): void {}
451
+ `;
452
+ const { checker, sourceFile } = createTestProgram(source);
453
+ let typeRef = null;
454
+ const visitor = (node) => {
455
+ if (ts.isTypeReferenceNode(node) &&
456
+ ts.isIdentifier(node.typeName) &&
457
+ node.typeName.text === "NonNullable") {
458
+ typeRef = node;
459
+ }
460
+ ts.forEachChild(node, visitor);
461
+ };
462
+ ts.forEachChild(sourceFile, visitor);
463
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "NonNullable", checker, stubConvertType);
464
+ expect(result).to.equal(null);
465
+ });
466
+ });
467
+ describe("Exclude<T, U>", () => {
468
+ it("should expand Exclude with literal strings", () => {
469
+ const source = `
470
+ type Result = Exclude<"a" | "b" | "c", "a">;
471
+ `;
472
+ const { checker, sourceFile } = createTestProgram(source);
473
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
474
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
475
+ // Should expand successfully (result is "b" | "c")
476
+ // Note: The exact IR kind depends on how TypeScript represents the resolved type
477
+ // which may vary. The key is that expansion succeeds and doesn't return null.
478
+ expect(result).not.to.equal(null);
479
+ });
480
+ it("should expand Exclude<string | number, number> to string", () => {
481
+ const source = `
482
+ type Result = Exclude<string | number, number>;
483
+ `;
484
+ const { checker, sourceFile } = createTestProgram(source);
485
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
486
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
487
+ expect(result).not.to.equal(null);
488
+ expect(result?.kind).to.equal("primitiveType");
489
+ if (result?.kind === "primitiveType") {
490
+ expect(result.name).to.equal("string");
491
+ }
492
+ });
493
+ it("should return never for Exclude<string, string>", () => {
494
+ const source = `
495
+ type Result = Exclude<string, string>;
496
+ `;
497
+ const { checker, sourceFile } = createTestProgram(source);
498
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
499
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
500
+ expect(result).not.to.equal(null);
501
+ expect(result?.kind).to.equal("neverType");
502
+ });
503
+ it("should return null for Exclude<T, U> where T is a type parameter", () => {
504
+ const source = `
505
+ function process<T>(data: Exclude<T, null>): void {}
506
+ `;
507
+ const { checker, sourceFile } = createTestProgram(source);
508
+ let typeRef = null;
509
+ const visitor = (node) => {
510
+ if (ts.isTypeReferenceNode(node) &&
511
+ ts.isIdentifier(node.typeName) &&
512
+ node.typeName.text === "Exclude") {
513
+ typeRef = node;
514
+ }
515
+ ts.forEachChild(node, visitor);
516
+ };
517
+ ts.forEachChild(sourceFile, visitor);
518
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
519
+ expect(result).to.equal(null);
520
+ });
521
+ });
522
+ describe("Extract<T, U>", () => {
523
+ it("should expand Extract with literal strings", () => {
524
+ const source = `
525
+ type Result = Extract<"a" | "b" | "c", "a" | "f">;
526
+ `;
527
+ const { checker, sourceFile } = createTestProgram(source);
528
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
529
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Extract", checker, stubConvertType);
530
+ expect(result).not.to.equal(null);
531
+ // Result should be "a" (the only common literal)
532
+ });
533
+ it("should expand Extract<string | number, string> to string", () => {
534
+ const source = `
535
+ type Result = Extract<string | number, string>;
536
+ `;
537
+ const { checker, sourceFile } = createTestProgram(source);
538
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
539
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Extract", checker, stubConvertType);
540
+ expect(result).not.to.equal(null);
541
+ expect(result?.kind).to.equal("primitiveType");
542
+ if (result?.kind === "primitiveType") {
543
+ expect(result.name).to.equal("string");
544
+ }
545
+ });
546
+ it("should return never for Extract<string, number>", () => {
547
+ const source = `
548
+ type Result = Extract<string, number>;
549
+ `;
550
+ const { checker, sourceFile } = createTestProgram(source);
551
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
552
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Extract", checker, stubConvertType);
553
+ expect(result).not.to.equal(null);
554
+ expect(result?.kind).to.equal("neverType");
555
+ });
556
+ });
557
+ describe("Distributive and never edge cases", () => {
558
+ it("should expand Exclude with never input to never", () => {
559
+ const source = `
560
+ type Result = Exclude<never, string>;
561
+ `;
562
+ const { checker, sourceFile } = createTestProgram(source);
563
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
564
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
565
+ expect(result).not.to.equal(null);
566
+ expect(result?.kind).to.equal("neverType");
567
+ });
568
+ it("should expand Extract with never input to never", () => {
569
+ const source = `
570
+ type Result = Extract<never, string>;
571
+ `;
572
+ const { checker, sourceFile } = createTestProgram(source);
573
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
574
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Extract", checker, stubConvertType);
575
+ expect(result).not.to.equal(null);
576
+ expect(result?.kind).to.equal("neverType");
577
+ });
578
+ it("should distribute Exclude over union - removing multiple types", () => {
579
+ const source = `
580
+ type Result = Exclude<"a" | "b" | "c" | "d", "a" | "c">;
581
+ `;
582
+ const { checker, sourceFile } = createTestProgram(source);
583
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
584
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
585
+ expect(result).not.to.equal(null);
586
+ // Should be a union of "b" | "d" (TypeScript checker resolves this)
587
+ });
588
+ it("should distribute Extract over union - extracting multiple types", () => {
589
+ const source = `
590
+ type Result = Extract<"a" | "b" | "c" | "d", "a" | "c" | "e">;
591
+ `;
592
+ const { checker, sourceFile } = createTestProgram(source);
593
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
594
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Extract", checker, stubConvertType);
595
+ expect(result).not.to.equal(null);
596
+ // Should be a union of "a" | "c" (TypeScript checker resolves this)
597
+ });
598
+ it("should handle Exclude with function types", () => {
599
+ const source = `
600
+ type Result = Exclude<string | number | (() => void), Function>;
601
+ `;
602
+ const { checker, sourceFile } = createTestProgram(source);
603
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
604
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
605
+ expect(result).not.to.equal(null);
606
+ // Should expand to string | number (function removed)
607
+ });
608
+ it("should distribute Exclude over mixed string and number literals", () => {
609
+ // Alice's review case: mixed literals with Exclude filtering by type
610
+ const source = `
611
+ type Mixed = ("a" | "b") | (1 | 2);
612
+ type OnlyNumbers = Exclude<Mixed, string>;
613
+ `;
614
+ const { checker, sourceFile } = createTestProgram(source);
615
+ const typeRef = findTypeAliasReference(sourceFile, "OnlyNumbers");
616
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
617
+ expect(result).not.to.equal(null);
618
+ // Should be 1 | 2 (string literals removed)
619
+ // TypeScript distributes over the union and removes string-assignable types
620
+ expect(result?.kind).to.equal("unionType");
621
+ });
622
+ it("should distribute Extract over mixed string and number literals", () => {
623
+ // Alice's review case: mixed literals with Extract filtering by type
624
+ const source = `
625
+ type Mixed = ("a" | "b") | (1 | 2);
626
+ type OnlyStrings = Extract<Mixed, string>;
627
+ `;
628
+ const { checker, sourceFile } = createTestProgram(source);
629
+ const typeRef = findTypeAliasReference(sourceFile, "OnlyStrings");
630
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Extract", checker, stubConvertType);
631
+ expect(result).not.to.equal(null);
632
+ // Should be "a" | "b" (number literals removed)
633
+ // TypeScript distributes over the union and keeps only string-assignable types
634
+ expect(result?.kind).to.equal("unionType");
635
+ });
636
+ it("should handle nested conditional types", () => {
637
+ const source = `
638
+ type Result = Exclude<Exclude<string | null | undefined, null>, undefined>;
639
+ `;
640
+ const { checker, sourceFile } = createTestProgram(source);
641
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
642
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Exclude", checker, stubConvertType);
643
+ expect(result).not.to.equal(null);
644
+ expect(result?.kind).to.equal("primitiveType");
645
+ if (result?.kind === "primitiveType") {
646
+ expect(result.name).to.equal("string");
647
+ }
648
+ });
649
+ });
650
+ describe("ReturnType<T>", () => {
651
+ it("should expand ReturnType<() => string> to string", () => {
652
+ const source = `
653
+ type Result = ReturnType<() => string>;
654
+ `;
655
+ const { checker, sourceFile } = createTestProgram(source);
656
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
657
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "ReturnType", checker, stubConvertType);
658
+ expect(result).not.to.equal(null);
659
+ expect(result?.kind).to.equal("primitiveType");
660
+ if (result?.kind === "primitiveType") {
661
+ expect(result.name).to.equal("string");
662
+ }
663
+ });
664
+ it("should expand ReturnType<(x: number) => boolean> to boolean", () => {
665
+ const source = `
666
+ type Result = ReturnType<(x: number) => boolean>;
667
+ `;
668
+ const { checker, sourceFile } = createTestProgram(source);
669
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
670
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "ReturnType", checker, stubConvertType);
671
+ expect(result).not.to.equal(null);
672
+ expect(result?.kind).to.equal("primitiveType");
673
+ if (result?.kind === "primitiveType") {
674
+ expect(result.name).to.equal("boolean");
675
+ }
676
+ });
677
+ it("should expand ReturnType with void return type", () => {
678
+ const source = `
679
+ type Result = ReturnType<() => void>;
680
+ `;
681
+ const { checker, sourceFile } = createTestProgram(source);
682
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
683
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "ReturnType", checker, stubConvertType);
684
+ expect(result).not.to.equal(null);
685
+ // void is handled by fallback
686
+ });
687
+ it("should expand ReturnType with union function types", () => {
688
+ const source = `
689
+ type Fn1 = () => string;
690
+ type Fn2 = () => number;
691
+ type Result = ReturnType<Fn1 | Fn2>;
692
+ `;
693
+ const { checker, sourceFile } = createTestProgram(source);
694
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
695
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "ReturnType", checker, stubConvertType);
696
+ expect(result).not.to.equal(null);
697
+ // Should be string | number
698
+ expect(result?.kind).to.equal("unionType");
699
+ });
700
+ it("should return null for ReturnType<T> where T is a type parameter", () => {
701
+ const source = `
702
+ function process<T extends () => unknown>(fn: T): ReturnType<T> {
703
+ return fn();
704
+ }
705
+ `;
706
+ const { checker, sourceFile } = createTestProgram(source);
707
+ let typeRef = null;
708
+ const visitor = (node) => {
709
+ if (ts.isTypeReferenceNode(node) &&
710
+ ts.isIdentifier(node.typeName) &&
711
+ node.typeName.text === "ReturnType") {
712
+ typeRef = node;
713
+ }
714
+ ts.forEachChild(node, visitor);
715
+ };
716
+ ts.forEachChild(sourceFile, visitor);
717
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "ReturnType", checker, stubConvertType);
718
+ expect(result).to.equal(null);
719
+ });
720
+ it("should expand ReturnType with typeof function", () => {
721
+ const source = `
722
+ function add(a: number, b: number): number {
723
+ return a + b;
724
+ }
725
+ type Result = ReturnType<typeof add>;
726
+ `;
727
+ const { checker, sourceFile } = createTestProgram(source);
728
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
729
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "ReturnType", checker, stubConvertType);
730
+ expect(result).not.to.equal(null);
731
+ expect(result?.kind).to.equal("primitiveType");
732
+ if (result?.kind === "primitiveType") {
733
+ expect(result.name).to.equal("number");
734
+ }
735
+ });
736
+ });
737
+ describe("Parameters<T>", () => {
738
+ it("should expand Parameters<(x: string, y: number) => void> to tuple", () => {
739
+ const source = `
740
+ type Result = Parameters<(x: string, y: number) => void>;
741
+ `;
742
+ const { checker, sourceFile } = createTestProgram(source);
743
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
744
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Parameters", checker, stubConvertType);
745
+ expect(result).not.to.equal(null);
746
+ // Parameters returns a tuple type - the exact representation depends on TypeScript
747
+ });
748
+ it("should handle Parameters<() => void> (empty tuple)", () => {
749
+ const source = `
750
+ type Result = Parameters<() => void>;
751
+ `;
752
+ const { checker, sourceFile } = createTestProgram(source);
753
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
754
+ // Empty tuple may return null (falls through to referenceType)
755
+ // or may return an expanded type - both are acceptable behaviors
756
+ // The key is that it doesn't throw an error
757
+ expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Parameters", checker, stubConvertType);
758
+ });
759
+ it("should expand Parameters with single parameter", () => {
760
+ const source = `
761
+ type Result = Parameters<(x: boolean) => void>;
762
+ `;
763
+ const { checker, sourceFile } = createTestProgram(source);
764
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
765
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Parameters", checker, stubConvertType);
766
+ expect(result).not.to.equal(null);
767
+ });
768
+ it("should return null for Parameters<T> where T is a type parameter", () => {
769
+ const source = `
770
+ function callWith<T extends (...args: unknown[]) => unknown>(
771
+ fn: T,
772
+ args: Parameters<T>
773
+ ): void {
774
+ fn(...args);
775
+ }
776
+ `;
777
+ const { checker, sourceFile } = createTestProgram(source);
778
+ let typeRef = null;
779
+ const visitor = (node) => {
780
+ if (ts.isTypeReferenceNode(node) &&
781
+ ts.isIdentifier(node.typeName) &&
782
+ node.typeName.text === "Parameters") {
783
+ typeRef = node;
784
+ }
785
+ ts.forEachChild(node, visitor);
786
+ };
787
+ ts.forEachChild(sourceFile, visitor);
788
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Parameters", checker, stubConvertType);
789
+ expect(result).to.equal(null);
790
+ });
791
+ it("should expand Parameters with typeof function", () => {
792
+ const source = `
793
+ function greet(name: string, age: number): void {}
794
+ type Result = Parameters<typeof greet>;
795
+ `;
796
+ const { checker, sourceFile } = createTestProgram(source);
797
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
798
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Parameters", checker, stubConvertType);
799
+ expect(result).not.to.equal(null);
800
+ });
801
+ });
802
+ describe("Awaited<T>", () => {
803
+ it("should expand Awaited<Promise<string>> to string", () => {
804
+ const source = `
805
+ type Result = Awaited<Promise<string>>;
806
+ `;
807
+ const { checker, sourceFile } = createTestProgram(source);
808
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
809
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Awaited", checker, stubConvertType);
810
+ expect(result).not.to.equal(null);
811
+ expect(result?.kind).to.equal("primitiveType");
812
+ if (result?.kind === "primitiveType") {
813
+ expect(result.name).to.equal("string");
814
+ }
815
+ });
816
+ it("should expand Awaited<Promise<Promise<number>>> recursively to number", () => {
817
+ const source = `
818
+ type Result = Awaited<Promise<Promise<number>>>;
819
+ `;
820
+ const { checker, sourceFile } = createTestProgram(source);
821
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
822
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Awaited", checker, stubConvertType);
823
+ expect(result).not.to.equal(null);
824
+ expect(result?.kind).to.equal("primitiveType");
825
+ if (result?.kind === "primitiveType") {
826
+ expect(result.name).to.equal("number");
827
+ }
828
+ });
829
+ it("should expand Awaited<string> to string (non-promise passthrough)", () => {
830
+ const source = `
831
+ type Result = Awaited<string>;
832
+ `;
833
+ const { checker, sourceFile } = createTestProgram(source);
834
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
835
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Awaited", checker, stubConvertType);
836
+ expect(result).not.to.equal(null);
837
+ expect(result?.kind).to.equal("primitiveType");
838
+ if (result?.kind === "primitiveType") {
839
+ expect(result.name).to.equal("string");
840
+ }
841
+ });
842
+ it("should expand Awaited with union of promises", () => {
843
+ const source = `
844
+ type Result = Awaited<Promise<string> | Promise<number>>;
845
+ `;
846
+ const { checker, sourceFile } = createTestProgram(source);
847
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
848
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Awaited", checker, stubConvertType);
849
+ expect(result).not.to.equal(null);
850
+ // Should be string | number
851
+ expect(result?.kind).to.equal("unionType");
852
+ });
853
+ it("should return null for Awaited<T> where T is a type parameter", () => {
854
+ const source = `
855
+ async function processAsync<T>(promise: Promise<T>): Promise<Awaited<T>> {
856
+ return await promise;
857
+ }
858
+ `;
859
+ const { checker, sourceFile } = createTestProgram(source);
860
+ let typeRef = null;
861
+ const visitor = (node) => {
862
+ if (ts.isTypeReferenceNode(node) &&
863
+ ts.isIdentifier(node.typeName) &&
864
+ node.typeName.text === "Awaited") {
865
+ typeRef = node;
866
+ }
867
+ ts.forEachChild(node, visitor);
868
+ };
869
+ ts.forEachChild(sourceFile, visitor);
870
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Awaited", checker, stubConvertType);
871
+ expect(result).to.equal(null);
872
+ });
873
+ it("should expand Awaited<null> to null", () => {
874
+ const source = `
875
+ type Result = Awaited<null>;
876
+ `;
877
+ const { checker, sourceFile } = createTestProgram(source);
878
+ const typeRef = findTypeAliasReference(sourceFile, "Result");
879
+ const result = expandConditionalUtilityType(assertDefined(typeRef, "typeRef should be defined"), "Awaited", checker, stubConvertType);
880
+ expect(result).not.to.equal(null);
881
+ expect(result?.kind).to.equal("primitiveType");
882
+ if (result?.kind === "primitiveType") {
883
+ expect(result.name).to.equal("null");
884
+ }
885
+ });
886
+ });
887
+ });
888
+ describe("Record Type Expansion", () => {
889
+ describe("Record with finite literal keys", () => {
890
+ it("should expand Record with string literal keys to IrObjectType", () => {
891
+ const source = `
892
+ type Config = Record<"a" | "b", number>;
893
+ `;
894
+ const { checker, sourceFile } = createTestProgram(source);
895
+ const typeRef = findTypeAliasReference(sourceFile, "Config");
896
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
897
+ expect(result).not.to.equal(null);
898
+ expect(result?.kind).to.equal("objectType");
899
+ expect(result?.members).to.have.length(2);
900
+ const propNames = result?.members
901
+ .filter((m) => m.kind === "propertySignature")
902
+ .map((m) => m.name);
903
+ expect(propNames).to.include("a");
904
+ expect(propNames).to.include("b");
905
+ });
906
+ it("should expand Record with number literal keys to IrObjectType", () => {
907
+ const source = `
908
+ type IndexedConfig = Record<1 | 2, string>;
909
+ `;
910
+ const { checker, sourceFile } = createTestProgram(source);
911
+ const typeRef = findTypeAliasReference(sourceFile, "IndexedConfig");
912
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
913
+ expect(result).not.to.equal(null);
914
+ expect(result?.kind).to.equal("objectType");
915
+ expect(result?.members).to.have.length(2);
916
+ // Numeric keys are prefixed with '_' to be valid C# identifiers
917
+ const propNames = result?.members
918
+ .filter((m) => m.kind === "propertySignature")
919
+ .map((m) => m.name);
920
+ expect(propNames).to.include("_1");
921
+ expect(propNames).to.include("_2");
922
+ });
923
+ it("should expand Record with mixed literal keys", () => {
924
+ const source = `
925
+ type MixedConfig = Record<"name" | "age" | "email", boolean>;
926
+ `;
927
+ const { checker, sourceFile } = createTestProgram(source);
928
+ const typeRef = findTypeAliasReference(sourceFile, "MixedConfig");
929
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
930
+ expect(result).not.to.equal(null);
931
+ expect(result?.kind).to.equal("objectType");
932
+ expect(result?.members).to.have.length(3);
933
+ });
934
+ });
935
+ describe("Record should fall back for non-literal keys", () => {
936
+ it("should return null for Record<string, T>", () => {
937
+ const source = `
938
+ type Dictionary = Record<string, number>;
939
+ `;
940
+ const { checker, sourceFile } = createTestProgram(source);
941
+ const typeRef = findTypeAliasReference(sourceFile, "Dictionary");
942
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
943
+ // Should return null - use IrDictionaryType instead
944
+ expect(result).to.equal(null);
945
+ });
946
+ it("should return null for Record<number, T>", () => {
947
+ const source = `
948
+ type NumberDictionary = Record<number, string>;
949
+ `;
950
+ const { checker, sourceFile } = createTestProgram(source);
951
+ const typeRef = findTypeAliasReference(sourceFile, "NumberDictionary");
952
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
953
+ // Should return null - use IrDictionaryType instead
954
+ expect(result).to.equal(null);
955
+ });
956
+ it("should return null for Record<K, T> where K is a type parameter", () => {
957
+ const source = `
958
+ function makeRecord<K extends string>(keys: K[]): Record<K, number> {
959
+ return {} as Record<K, number>;
960
+ }
961
+ `;
962
+ const { checker, sourceFile } = createTestProgram(source);
963
+ let typeRef = null;
964
+ const visitor = (node) => {
965
+ if (ts.isTypeReferenceNode(node) &&
966
+ ts.isIdentifier(node.typeName) &&
967
+ node.typeName.text === "Record") {
968
+ typeRef = node;
969
+ return; // Take first one (return type)
970
+ }
971
+ ts.forEachChild(node, visitor);
972
+ };
973
+ ts.forEachChild(sourceFile, visitor);
974
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
975
+ // Should return null - type parameter can't be expanded
976
+ expect(result).to.equal(null);
977
+ });
978
+ it("should return null for Record<PropertyKey, T> (complex key type)", () => {
979
+ // PropertyKey is string | number | symbol - not a finite set of literals
980
+ // This should NOT be expanded to objectType or dictionaryType
981
+ const source = `
982
+ type AnyKeyRecord = Record<PropertyKey, number>;
983
+ `;
984
+ const { checker, sourceFile } = createTestProgram(source);
985
+ const typeRef = findTypeAliasReference(sourceFile, "AnyKeyRecord");
986
+ const result = expandRecordType(assertDefined(typeRef, "typeRef should be defined"), checker, stubConvertType);
987
+ // Should return null - PropertyKey is not a finite set of literals
988
+ // and should fall through to referenceType (not dictionaryType)
989
+ expect(result).to.equal(null);
990
+ });
991
+ });
992
+ describe("Record<K, V> full type conversion (integration test)", () => {
993
+ it("should convert Record<K, V> with type parameter K to referenceType, not dictionaryType", () => {
994
+ // This tests the full convertTypeReference flow, not just expandRecordType
995
+ // The bug was: Record<K, V> where K is a type parameter was incorrectly
996
+ // converted to dictionaryType instead of referenceType
997
+ const source = `
998
+ interface Wrapper<K extends string> {
999
+ data: Record<K, number>;
1000
+ }
1001
+ `;
1002
+ const { checker, sourceFile } = createTestProgram(source);
1003
+ // Find the Record<K, number> type reference in the interface property
1004
+ let typeRef = null;
1005
+ const visitor = (node) => {
1006
+ if (ts.isTypeReferenceNode(node) &&
1007
+ ts.isIdentifier(node.typeName) &&
1008
+ node.typeName.text === "Record") {
1009
+ typeRef = node;
1010
+ }
1011
+ ts.forEachChild(node, visitor);
1012
+ };
1013
+ ts.forEachChild(sourceFile, visitor);
1014
+ // Get the key type node and check its flags
1015
+ expect(typeRef).not.to.equal(null);
1016
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1017
+ const foundTypeRef = typeRef;
1018
+ const keyTypeNode = foundTypeRef.typeArguments?.[0];
1019
+ expect(keyTypeNode).not.to.equal(undefined);
1020
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
1021
+ const keyTsType = checker.getTypeAtLocation(keyTypeNode);
1022
+ // The key type should be a type parameter, not string
1023
+ expect(!!(keyTsType.flags & ts.TypeFlags.TypeParameter)).to.equal(true);
1024
+ expect(!!(keyTsType.flags & ts.TypeFlags.String)).to.equal(false);
1025
+ // This confirms the fix: when K is a type parameter, the code should
1026
+ // fall through to referenceType instead of creating a dictionaryType
1027
+ });
1028
+ });
1029
+ });
1030
+ //# sourceMappingURL=utility-types.test.js.map