diff --git a/src/handlers/typebox/indexed-access-type-handler.ts b/src/handlers/typebox/indexed-access-type-handler.ts index a3066d3..74b2408 100644 --- a/src/handlers/typebox/indexed-access-type-handler.ts +++ b/src/handlers/typebox/indexed-access-type-handler.ts @@ -12,9 +12,94 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler { const objectType = typeNode.getObjectTypeNode() const indexType = typeNode.getIndexTypeNode() + // Handle special case: typeof A[number] where A is a readonly tuple + if ( + objectType?.isKind(ts.SyntaxKind.TypeQuery) && + indexType?.isKind(ts.SyntaxKind.NumberKeyword) + ) { + return this.handleTypeofArrayAccess(objectType, typeNode) + } + const typeboxObjectType = this.getTypeBoxType(objectType) const typeboxIndexType = this.getTypeBoxType(indexType) return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) } + + private handleTypeofArrayAccess( + typeQuery: Node, + indexedAccessType: IndexedAccessTypeNode, + ): ts.Expression { + const typeQueryNode = typeQuery.asKindOrThrow(ts.SyntaxKind.TypeQuery) + const exprName = typeQueryNode.getExprName() + + // Get the referenced type name (e.g., "A" from "typeof A") + if (Node.isIdentifier(exprName)) { + const typeName = exprName.getText() + const sourceFile = indexedAccessType.getSourceFile() + + // First try to find a type alias declaration + const typeAlias = sourceFile.getTypeAlias(typeName) + if (typeAlias) { + const tupleUnion = this.extractTupleUnion(typeAlias.getTypeNode()) + if (tupleUnion) { + return tupleUnion + } + } + + // Then try to find a variable declaration + const variableDeclaration = sourceFile.getVariableDeclaration(typeName) + if (variableDeclaration) { + const tupleUnion = this.extractTupleUnion(variableDeclaration.getTypeNode()) + if (tupleUnion) { + return tupleUnion + } + } + } + + // Fallback to default Index behavior + const typeboxObjectType = this.getTypeBoxType(typeQuery) + const typeboxIndexType = this.getTypeBoxType(indexedAccessType.getIndexTypeNode()) + return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType]) + } + + private extractTupleUnion(typeNode: Node | undefined): ts.Expression | null { + if (!typeNode) return null + + let actualTupleType: Node | undefined = typeNode + + // Handle readonly modifier (TypeOperator) + if (typeNode.isKind(ts.SyntaxKind.TypeOperator)) { + const typeOperator = typeNode.asKindOrThrow(ts.SyntaxKind.TypeOperator) + actualTupleType = typeOperator.getTypeNode() + } + + // Check if it's a tuple type + if (actualTupleType?.isKind(ts.SyntaxKind.TupleType)) { + const tupleType = actualTupleType.asKindOrThrow(ts.SyntaxKind.TupleType) + const elements = tupleType.getElements() + + // Extract literal types from tuple elements + const literalTypes: ts.Expression[] = [] + for (const element of elements) { + if (element.isKind(ts.SyntaxKind.LiteralType)) { + const literalTypeNode = element.asKindOrThrow(ts.SyntaxKind.LiteralType) + const literal = literalTypeNode.getLiteral() + + if (literal.isKind(ts.SyntaxKind.StringLiteral)) { + const stringLiteral = literal.asKindOrThrow(ts.SyntaxKind.StringLiteral) + const value = stringLiteral.getLiteralValue() + literalTypes.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(value)])) + } + } + } + + // Return union of literal types if we found any + if (literalTypes.length > 0) { + return makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literalTypes)]) + } + } + + return null + } } diff --git a/tests/handlers/typebox/array-types.test.ts b/tests/handlers/typebox/array-types.test.ts index f90d817..7111573 100644 --- a/tests/handlers/typebox/array-types.test.ts +++ b/tests/handlers/typebox/array-types.test.ts @@ -15,10 +15,10 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Array(Type.String()); + const A = Type.Array(Type.String()); - type A = Static; - `), + type A = Static; + `), ) }) @@ -27,61 +27,83 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Array(Type.String()); + const A = Type.Array(Type.String()); - type A = Static; - `), + type A = Static; + `), + ) + }) + + test('array spread', () => { + const sourceFile = createSourceFile( + project, + ` + declare const A: readonly ["a", "b", "c"]; + type A = typeof A[number]; + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]); + + type A = Static; + `), ) }) test('Union', () => { const sourceFile = createSourceFile( project, - `type A = number; - type B = string; - type T = A | B;`, + ` + type A = number; + type B = string; + type T = A | B; + `, ) expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const A = Type.Number(); + const A = Type.Number(); - type A = Static; + type A = Static; - const B = Type.String(); + const B = Type.String(); - type B = Static; + type B = Static; - const T = Type.Union([A, B]); + const T = Type.Union([A, B]); - type T = Static; - `), + type T = Static; + `), ) }) test('Intersect', () => { const sourceFile = createSourceFile( project, - `type T = { - x: number; - } & { - y: string; - };`, + ` + type T = { + x: number; + } & { + y: string; + }; + `, ) expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Intersect([ - Type.Object({ - x: Type.Number(), - }), - Type.Object({ - y: Type.String(), - }), - ]); - - type T = Static; - `), + const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.String(), + }), + ]); + + type T = Static; + `), ) }) @@ -90,10 +112,10 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); + const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); - type T = Static; - `), + type T = Static; + `), ) }) }) @@ -104,10 +126,10 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - export const A = Type.Array(Type.String()); + export const A = Type.Array(Type.String()); - export type A = Static; - `), + export type A = Static; + `), ) }) @@ -116,78 +138,95 @@ describe('Array types', () => { expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - export const A = Type.Array(Type.String()); + export const A = Type.Array(Type.String()); - export type A = Static; - `), + export type A = Static; + `), + ) + }) + + test('array spread', () => { + const sourceFile = createSourceFile( + project, + ` + export declare const A: readonly ["a", "b", "c"]; + export type A = typeof A[number]; + `, + ) + + expect(generateFormattedCode(sourceFile)).resolves.toBe( + formatWithPrettier(` + export const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]); + + export type A = Static; + `), ) }) test('Union', () => { const sourceFile = createSourceFile( project, - `export type A = number; - export type B = string; - export type T = A | B;`, + ` + export type A = number; + export type B = string; + export type T = A | B; + `, ) expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - export const A = Type.Number(); + export const A = Type.Number(); - export type A = Static; + export type A = Static; - export const B = Type.String(); + export const B = Type.String(); - export type B = Static; + export type B = Static; - export const T = Type.Union([A, B]); + export const T = Type.Union([A, B]); - export type T = Static; - `), + export type T = Static; + `), ) }) test('Intersect', () => { const sourceFile = createSourceFile( project, - `export type T = { - x: number; - } & { - y: string; - };`, + ` + export type T = { + x: number; + } & { + y: string; + }; + `, ) expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - export const T = Type.Intersect([ - Type.Object({ - x: Type.Number(), - }), - Type.Object({ - y: Type.String(), - }), - ]); - - export type T = Static; - `), + export const T = Type.Intersect([ + Type.Object({ + x: Type.Number(), + }), + Type.Object({ + y: Type.String(), + }), + ]); + + export type T = Static; + `), ) }) test('Literal', () => { - const sourceFile = createSourceFile( - project, - ` - export type T = "a" | "b"; - `, - ) + const sourceFile = createSourceFile(project, 'export type T = "a" | "b";') expect(generateFormattedCode(sourceFile)).resolves.toBe( formatWithPrettier(` - export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); + export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]); - export type T = Static; - `), + export type T = Static; + `), ) }) })