Skip to content

Commit 90e2306

Browse files
authored
feat: extend indexed array access support (#5)
1 parent 58f1a1d commit 90e2306

File tree

2 files changed

+200
-76
lines changed

2 files changed

+200
-76
lines changed

src/handlers/typebox/indexed-access-type-handler.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,94 @@ export class IndexedAccessTypeHandler extends BaseTypeHandler {
1212
const objectType = typeNode.getObjectTypeNode()
1313
const indexType = typeNode.getIndexTypeNode()
1414

15+
// Handle special case: typeof A[number] where A is a readonly tuple
16+
if (
17+
objectType?.isKind(ts.SyntaxKind.TypeQuery) &&
18+
indexType?.isKind(ts.SyntaxKind.NumberKeyword)
19+
) {
20+
return this.handleTypeofArrayAccess(objectType, typeNode)
21+
}
22+
1523
const typeboxObjectType = this.getTypeBoxType(objectType)
1624
const typeboxIndexType = this.getTypeBoxType(indexType)
1725

1826
return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType])
1927
}
28+
29+
private handleTypeofArrayAccess(
30+
typeQuery: Node,
31+
indexedAccessType: IndexedAccessTypeNode,
32+
): ts.Expression {
33+
const typeQueryNode = typeQuery.asKindOrThrow(ts.SyntaxKind.TypeQuery)
34+
const exprName = typeQueryNode.getExprName()
35+
36+
// Get the referenced type name (e.g., "A" from "typeof A")
37+
if (Node.isIdentifier(exprName)) {
38+
const typeName = exprName.getText()
39+
const sourceFile = indexedAccessType.getSourceFile()
40+
41+
// First try to find a type alias declaration
42+
const typeAlias = sourceFile.getTypeAlias(typeName)
43+
if (typeAlias) {
44+
const tupleUnion = this.extractTupleUnion(typeAlias.getTypeNode())
45+
if (tupleUnion) {
46+
return tupleUnion
47+
}
48+
}
49+
50+
// Then try to find a variable declaration
51+
const variableDeclaration = sourceFile.getVariableDeclaration(typeName)
52+
if (variableDeclaration) {
53+
const tupleUnion = this.extractTupleUnion(variableDeclaration.getTypeNode())
54+
if (tupleUnion) {
55+
return tupleUnion
56+
}
57+
}
58+
}
59+
60+
// Fallback to default Index behavior
61+
const typeboxObjectType = this.getTypeBoxType(typeQuery)
62+
const typeboxIndexType = this.getTypeBoxType(indexedAccessType.getIndexTypeNode())
63+
return makeTypeCall('Index', [typeboxObjectType, typeboxIndexType])
64+
}
65+
66+
private extractTupleUnion(typeNode: Node | undefined): ts.Expression | null {
67+
if (!typeNode) return null
68+
69+
let actualTupleType: Node | undefined = typeNode
70+
71+
// Handle readonly modifier (TypeOperator)
72+
if (typeNode.isKind(ts.SyntaxKind.TypeOperator)) {
73+
const typeOperator = typeNode.asKindOrThrow(ts.SyntaxKind.TypeOperator)
74+
actualTupleType = typeOperator.getTypeNode()
75+
}
76+
77+
// Check if it's a tuple type
78+
if (actualTupleType?.isKind(ts.SyntaxKind.TupleType)) {
79+
const tupleType = actualTupleType.asKindOrThrow(ts.SyntaxKind.TupleType)
80+
const elements = tupleType.getElements()
81+
82+
// Extract literal types from tuple elements
83+
const literalTypes: ts.Expression[] = []
84+
for (const element of elements) {
85+
if (element.isKind(ts.SyntaxKind.LiteralType)) {
86+
const literalTypeNode = element.asKindOrThrow(ts.SyntaxKind.LiteralType)
87+
const literal = literalTypeNode.getLiteral()
88+
89+
if (literal.isKind(ts.SyntaxKind.StringLiteral)) {
90+
const stringLiteral = literal.asKindOrThrow(ts.SyntaxKind.StringLiteral)
91+
const value = stringLiteral.getLiteralValue()
92+
literalTypes.push(makeTypeCall('Literal', [ts.factory.createStringLiteral(value)]))
93+
}
94+
}
95+
}
96+
97+
// Return union of literal types if we found any
98+
if (literalTypes.length > 0) {
99+
return makeTypeCall('Union', [ts.factory.createArrayLiteralExpression(literalTypes)])
100+
}
101+
}
102+
103+
return null
104+
}
20105
}

tests/handlers/typebox/array-types.test.ts

Lines changed: 115 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ describe('Array types', () => {
1515

1616
expect(generateFormattedCode(sourceFile)).resolves.toBe(
1717
formatWithPrettier(`
18-
const A = Type.Array(Type.String());
18+
const A = Type.Array(Type.String());
1919
20-
type A = Static<typeof A>;
21-
`),
20+
type A = Static<typeof A>;
21+
`),
2222
)
2323
})
2424

@@ -27,61 +27,83 @@ describe('Array types', () => {
2727

2828
expect(generateFormattedCode(sourceFile)).resolves.toBe(
2929
formatWithPrettier(`
30-
const A = Type.Array(Type.String());
30+
const A = Type.Array(Type.String());
3131
32-
type A = Static<typeof A>;
33-
`),
32+
type A = Static<typeof A>;
33+
`),
34+
)
35+
})
36+
37+
test('array spread', () => {
38+
const sourceFile = createSourceFile(
39+
project,
40+
`
41+
declare const A: readonly ["a", "b", "c"];
42+
type A = typeof A[number];
43+
`,
44+
)
45+
46+
expect(generateFormattedCode(sourceFile)).resolves.toBe(
47+
formatWithPrettier(`
48+
const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]);
49+
50+
type A = Static<typeof A>;
51+
`),
3452
)
3553
})
3654

3755
test('Union', () => {
3856
const sourceFile = createSourceFile(
3957
project,
40-
`type A = number;
41-
type B = string;
42-
type T = A | B;`,
58+
`
59+
type A = number;
60+
type B = string;
61+
type T = A | B;
62+
`,
4363
)
4464

4565
expect(generateFormattedCode(sourceFile)).resolves.toBe(
4666
formatWithPrettier(`
47-
const A = Type.Number();
67+
const A = Type.Number();
4868
49-
type A = Static<typeof A>;
69+
type A = Static<typeof A>;
5070
51-
const B = Type.String();
71+
const B = Type.String();
5272
53-
type B = Static<typeof B>;
73+
type B = Static<typeof B>;
5474
55-
const T = Type.Union([A, B]);
75+
const T = Type.Union([A, B]);
5676
57-
type T = Static<typeof T>;
58-
`),
77+
type T = Static<typeof T>;
78+
`),
5979
)
6080
})
6181

6282
test('Intersect', () => {
6383
const sourceFile = createSourceFile(
6484
project,
65-
`type T = {
66-
x: number;
67-
} & {
68-
y: string;
69-
};`,
85+
`
86+
type T = {
87+
x: number;
88+
} & {
89+
y: string;
90+
};
91+
`,
7092
)
7193

7294
expect(generateFormattedCode(sourceFile)).resolves.toBe(
7395
formatWithPrettier(`
74-
const T = Type.Intersect([
75-
Type.Object({
76-
x: Type.Number(),
77-
}),
78-
Type.Object({
79-
y: Type.String(),
80-
}),
81-
]);
82-
83-
type T = Static<typeof T>;
84-
`),
96+
const T = Type.Intersect([
97+
Type.Object({
98+
x: Type.Number(),
99+
}),
100+
Type.Object({
101+
y: Type.String(),
102+
}),
103+
]);
104+
105+
type T = Static<typeof T>;
106+
`),
85107
)
86108
})
87109

@@ -90,10 +112,10 @@ describe('Array types', () => {
90112

91113
expect(generateFormattedCode(sourceFile)).resolves.toBe(
92114
formatWithPrettier(`
93-
const T = Type.Union([Type.Literal("a"), Type.Literal("b")]);
115+
const T = Type.Union([Type.Literal("a"), Type.Literal("b")]);
94116
95-
type T = Static<typeof T>;
96-
`),
117+
type T = Static<typeof T>;
118+
`),
97119
)
98120
})
99121
})
@@ -104,10 +126,10 @@ describe('Array types', () => {
104126

105127
expect(generateFormattedCode(sourceFile)).resolves.toBe(
106128
formatWithPrettier(`
107-
export const A = Type.Array(Type.String());
129+
export const A = Type.Array(Type.String());
108130
109-
export type A = Static<typeof A>;
110-
`),
131+
export type A = Static<typeof A>;
132+
`),
111133
)
112134
})
113135

@@ -116,78 +138,95 @@ describe('Array types', () => {
116138

117139
expect(generateFormattedCode(sourceFile)).resolves.toBe(
118140
formatWithPrettier(`
119-
export const A = Type.Array(Type.String());
141+
export const A = Type.Array(Type.String());
120142
121-
export type A = Static<typeof A>;
122-
`),
143+
export type A = Static<typeof A>;
144+
`),
145+
)
146+
})
147+
148+
test('array spread', () => {
149+
const sourceFile = createSourceFile(
150+
project,
151+
`
152+
export declare const A: readonly ["a", "b", "c"];
153+
export type A = typeof A[number];
154+
`,
155+
)
156+
157+
expect(generateFormattedCode(sourceFile)).resolves.toBe(
158+
formatWithPrettier(`
159+
export const A = Type.Union([Type.Literal("a"), Type.Literal("b"), Type.Literal("c")]);
160+
161+
export type A = Static<typeof A>;
162+
`),
123163
)
124164
})
125165

126166
test('Union', () => {
127167
const sourceFile = createSourceFile(
128168
project,
129-
`export type A = number;
130-
export type B = string;
131-
export type T = A | B;`,
169+
`
170+
export type A = number;
171+
export type B = string;
172+
export type T = A | B;
173+
`,
132174
)
133175

134176
expect(generateFormattedCode(sourceFile)).resolves.toBe(
135177
formatWithPrettier(`
136-
export const A = Type.Number();
178+
export const A = Type.Number();
137179
138-
export type A = Static<typeof A>;
180+
export type A = Static<typeof A>;
139181
140-
export const B = Type.String();
182+
export const B = Type.String();
141183
142-
export type B = Static<typeof B>;
184+
export type B = Static<typeof B>;
143185
144-
export const T = Type.Union([A, B]);
186+
export const T = Type.Union([A, B]);
145187
146-
export type T = Static<typeof T>;
147-
`),
188+
export type T = Static<typeof T>;
189+
`),
148190
)
149191
})
150192

151193
test('Intersect', () => {
152194
const sourceFile = createSourceFile(
153195
project,
154-
`export type T = {
155-
x: number;
156-
} & {
157-
y: string;
158-
};`,
196+
`
197+
export type T = {
198+
x: number;
199+
} & {
200+
y: string;
201+
};
202+
`,
159203
)
160204

161205
expect(generateFormattedCode(sourceFile)).resolves.toBe(
162206
formatWithPrettier(`
163-
export const T = Type.Intersect([
164-
Type.Object({
165-
x: Type.Number(),
166-
}),
167-
Type.Object({
168-
y: Type.String(),
169-
}),
170-
]);
171-
172-
export type T = Static<typeof T>;
173-
`),
207+
export const T = Type.Intersect([
208+
Type.Object({
209+
x: Type.Number(),
210+
}),
211+
Type.Object({
212+
y: Type.String(),
213+
}),
214+
]);
215+
216+
export type T = Static<typeof T>;
217+
`),
174218
)
175219
})
176220

177221
test('Literal', () => {
178-
const sourceFile = createSourceFile(
179-
project,
180-
`
181-
export type T = "a" | "b";
182-
`,
183-
)
222+
const sourceFile = createSourceFile(project, 'export type T = "a" | "b";')
184223

185224
expect(generateFormattedCode(sourceFile)).resolves.toBe(
186225
formatWithPrettier(`
187-
export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]);
226+
export const T = Type.Union([Type.Literal("a"), Type.Literal("b")]);
188227
189-
export type T = Static<typeof T>;
190-
`),
228+
export type T = Static<typeof T>;
229+
`),
191230
)
192231
})
193232
})

0 commit comments

Comments
 (0)