Skip to content
File

Blob: types/src/transforms/class-to-interface.ts

typescript396 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5import ts from "typescript";
6 
7/**
8 * Transforms an array of classes to an interface/variable pair, preserving the ability to construct the class
9 * and call static methods.
10 *
11 * @example
12 *
13 * class MyClass<T> {
14 * constructor(str: string): MyClass<T>;
15 * prop: T;
16 * method(): void {}
17 * static staticMethod(str?: string): void {}
18 * }
19 *
20 * // Becomes this:
21 *
22 * declare var MyClass: {
23 * prototype: MyClass;
24 * new <T>(str: string): MyClass<T>;
25 * staticMethod(str?: string): void;
26 * }
27 * interface MyClass<T = void, U = void> {
28 * prop: T;
29 * method(): U;
30 * }
31 *
32 * NB:
33 * 1. Generics are preserved and provided to the `new` method on the var declaration.
34 * 2. Static methods are added to the var declaration instead of the interface.
35 */
36export function createClassToInterfaceTransformer(
37 classNames: string[]
38): ts.TransformerFactory<ts.SourceFile> {
39 return (context) => {
40 const visitor: ts.Visitor = (node) => {
41 if (
42 ts.isClassDeclaration(node) &&
43 node.name &&
44 classNames.includes(node.name.text)
45 ) {
46 return transformClassToInterface(node, context);
47 }
48 return ts.visitEachChild(node, visitor, context);
49 };
50 
51 return (sourceFile) => {
52 const transformedNodes = ts.visitNodes(sourceFile.statements, visitor);
53 const filteredNodes = transformedNodes.filter(ts.isStatement);
54 return context.factory.updateSourceFile(sourceFile, filteredNodes);
55 };
56 };
57}
58 
59/**
60 * Transforms a TypeScript class declaration into an interface and a variable declaration.
61 * Used where you want to separate the type definition (interface)
62 * from the runtime representation (variable declaration) of a class.
63 */
64function transformClassToInterface(
65 node: ts.ClassDeclaration,
66 context: ts.TransformationContext
67): ts.Statement[] {
68 const interfaceDeclaration = createInterfaceDeclaration(node, context);
69 const varDeclaration = createVariableDeclaration(node, context);
70 return [varDeclaration, interfaceDeclaration];
71}
72 
73/**
74 * Creates an interface declaration from a class declaration.
75 * Extracts class members and converts them into interface members,
76 * preserving access modifiers and type parameters.
77 */
78function createInterfaceDeclaration(
79 node: ts.ClassDeclaration,
80 context: ts.TransformationContext
81): ts.InterfaceDeclaration {
82 const interfaceMembers = transformClassMembers(node.members, context, false);
83 return context.factory.createInterfaceDeclaration(
84 getAccessModifiers(ts.getModifiers(node)),
85 node.name,
86 node.typeParameters,
87 node.heritageClauses,
88 interfaceMembers
89 );
90}
91 
92/**
93 * Transforms class members into interface type elements.
94 * Filters and converts class elements into a format suitable for interfaces,
95 * optionally including static members.
96 */
97function transformClassMembers(
98 members: ts.NodeArray<ts.ClassElement>,
99 context: ts.TransformationContext,
100 includeStatic: boolean
101): ts.TypeElement[] {
102 return members
103 .map((member) =>
104 transformClassMemberToInterface(member, context, includeStatic)
105 )
106 .filter((member): member is ts.TypeElement => member !== undefined);
107}
108 
109/**
110 * Transforms a single class member into an interface element.
111 * Handles different types of class elements, such as properties and methods,
112 * and applies access modifiers appropriately.
113 *
114 * @example
115 * // Given the following class declarations:
116 *
117 * myMethod(): void {}
118 * private myPrivateProperty: string;
119 *
120 * // The function will produce an interface declarations similar to:
121 *
122 * myMethod(): void;
123 *
124 * Note: Private members like `myPrivateProperty` are not included in the interface.
125 */
126function transformClassMemberToInterface(
127 member: ts.ClassElement,
128 context: ts.TransformationContext,
129 includeStatic: boolean
130): ts.TypeElement | undefined {
131 const modifiers = ts.canHaveModifiers(member)
132 ? ts.getModifiers(member)
133 : undefined;
134 const isStatic =
135 modifiers?.some((mod) => mod.kind === ts.SyntaxKind.StaticKeyword) ?? false;
136 
137 if (isStatic !== includeStatic) {
138 return undefined;
139 }
140 
141 const isPrivate =
142 modifiers?.some((mod) => mod.kind === ts.SyntaxKind.PrivateKeyword) ??
143 false;
144 
145 if (isPrivate) {
146 return undefined;
147 }
148 
149 const accessModifiers = getAccessModifiers(modifiers);
150 
151 if (ts.isPropertyDeclaration(member)) {
152 return createPropertySignature(member, accessModifiers, context);
153 } else if (ts.isMethodDeclaration(member)) {
154 return createMethodSignature(member, accessModifiers, context);
155 } else if (ts.isGetAccessor(member)) {
156 return createGetAccessorSignature(member, accessModifiers, context);
157 } else if (ts.isSetAccessor(member) || ts.isConstructorDeclaration(member)) {
158 return undefined;
159 }
160 
161 console.warn(`Unhandled member type: ${ts.SyntaxKind[member.kind]}`);
162 return undefined;
163}
164 
165/**
166 * Creates a property signature for an interface from a class property declaration.
167 * Preserves access modifiers and optionality.
168 *
169 * @example
170 * // Given a TypeScript class property declaration:
171 *
172 * public optionalProp?: string;
173 *
174 * // The `createPropertySignature` function will produce an interface property signature:
175 *
176 * optionalProp?: string;
177 */
178function createPropertySignature(
179 member: ts.PropertyDeclaration,
180 modifiers: ts.Modifier[] | undefined,
181 context: ts.TransformationContext
182): ts.PropertySignature {
183 return context.factory.createPropertySignature(
184 modifiers,
185 member.name,
186 member.questionToken,
187 member.type
188 );
189}
190 
191/**
192 * Creates a method signature for an interface from a class method declaration.
193 * Handles method parameters and return types.
194 *
195 * @example
196 * // Given a TypeScript class method declaration:
197 *
198 * public doSomething(param: number): string {
199 * return param.toString();
200 * }
201 *
202 * // The `createMethodSignature` function will produce an interface method signature:
203 *
204 * doSomething(param: number): string;
205 */
206function createMethodSignature(
207 member: ts.MethodDeclaration,
208 modifiers: ts.Modifier[] | undefined,
209 context: ts.TransformationContext
210): ts.MethodSignature {
211 return context.factory.createMethodSignature(
212 modifiers,
213 member.name,
214 member.questionToken,
215 member.typeParameters,
216 member.parameters,
217 member.type
218 );
219}
220 
221/**
222 * Creates a property signature for an interface from a class `get` accessor declaration.
223 * Used to represent getter methods as properties in interfaces.
224 *
225 * @example
226 * // Given a TypeScript class with a getter:
227 *
228 * get value(): number {
229 * return 42;
230 * }
231 *
232 * // The `createGetAccessorSignature` function will produce an interface property signature:
233 *
234 * value: number;
235 */
236function createGetAccessorSignature(
237 member: ts.GetAccessorDeclaration,
238 modifiers: ts.Modifier[] | undefined,
239 context: ts.TransformationContext
240): ts.PropertySignature {
241 return context.factory.createPropertySignature(
242 modifiers,
243 member.name,
244 undefined,
245 member.type
246 );
247}
248 
249/**
250 * Creates a variable declaration for a class, representing its runtime type.
251 * Declares a variable with the class name and its associated type.
252 *
253 * @example
254 * // Given a TypeScript class declaration:
255 *
256 * class Example {
257 * static staticMethod(): void {}
258 * constructor(public value: number) {}
259 * }
260 *
261 * // The `createVariableDeclaration` function will produce a variable declaration:
262 *
263 * declare var Example: {
264 * prototype: Example;
265 * new (value: number): Example;
266 * staticMethod(): void;
267 * };
268 */
269function createVariableDeclaration(
270 node: ts.ClassDeclaration,
271 context: ts.TransformationContext
272): ts.VariableStatement {
273 return context.factory.createVariableStatement(
274 [context.factory.createModifier(ts.SyntaxKind.DeclareKeyword)],
275 context.factory.createVariableDeclarationList(
276 [
277 context.factory.createVariableDeclaration(
278 node.name,
279 undefined,
280 createClassType(node, context)
281 ),
282 ],
283 ts.NodeFlags.None
284 )
285 );
286}
287 
288/**
289 * Creates a type literal node representing the static members and prototype of a class.
290 * Used to define the type structure of a class's static side.
291 *
292 * @example
293 * // Given a TypeScript class with static members:
294 *
295 * class Example {
296 * constructor(public value: number) {}
297 * static staticMethod(): void {}
298 * }
299 *
300 * // The `createClassType` function will produce a type literal node:
301 *
302 * {
303 * prototype: Example;
304 * new (value: number): Example;
305 * staticMethod(): void;
306 * }
307 */
308function createClassType(
309 node: ts.ClassDeclaration,
310 context: ts.TransformationContext
311): ts.TypeLiteralNode {
312 const staticMembers = transformClassMembers(node.members, context, true);
313 return context.factory.createTypeLiteralNode([
314 createPrototypeProperty(node, context),
315 createConstructSignature(node, context),
316 ...staticMembers,
317 ]);
318}
319 
320/**
321 * Creates a construct signature for a class, representing its constructor.
322 * Includes type parameters and parameter types in the signature.
323 *
324 * @example
325 * // Given a TypeScript class constructor:
326 *
327 * class Example<T> {
328 * constructor(public value: T) {}
329 * }
330 *
331 * // The `createConstructSignature` function will produce a construct signature:
332 *
333 * new <T>(value: T): Example<T>;
334 */
335function createConstructSignature(
336 node: ts.ClassDeclaration,
337 context: ts.TransformationContext
338): ts.ConstructSignatureDeclaration {
339 const constructorDeclaration = node.members.find(ts.isConstructorDeclaration);
340 const typeParameters = node.typeParameters;
341 
342 const returnType = context.factory.createTypeReferenceNode(
343 node.name,
344 typeParameters?.map((param) =>
345 context.factory.createTypeReferenceNode(param.name, undefined)
346 )
347 );
348 
349 return context.factory.createConstructSignature(
350 typeParameters,
351 constructorDeclaration?.parameters ?? [],
352 returnType
353 );
354}
355 
356/**
357 * Creates a property signature for the prototype property of a class.
358 * Used to represent the prototype chain in the class type.
359 *
360 * @example
361 * // Given a TypeScript class:
362 *
363 * class Example {}
364 *
365 * // The `createPrototypeProperty` function will produce a property signature:
366 *
367 * prototype: Example;
368 */
369function createPrototypeProperty(
370 node: ts.ClassDeclaration,
371 context: ts.TransformationContext
372): ts.PropertySignature {
373 return context.factory.createPropertySignature(
374 undefined,
375 "prototype",
376 undefined,
377 context.factory.createTypeReferenceNode(node.name, undefined)
378 );
379}
380 
381/**
382 * Filters and returns the access modifiers applicable to a class member.
383 * Extracts modifiers such as readonly, public, protected, and private.
384 */
385function getAccessModifiers(
386 modifiers: readonly ts.Modifier[] | undefined
387): ts.Modifier[] | undefined {
388 return modifiers?.filter(
389 (mod) =>
390 mod.kind === ts.SyntaxKind.ReadonlyKeyword ||
391 mod.kind === ts.SyntaxKind.PublicKeyword ||
392 mod.kind === ts.SyntaxKind.ProtectedKeyword ||
393 mod.kind === ts.SyntaxKind.PrivateKeyword
394 );
395}