From d087673f8e952a8a0dcffe773bfbe0420c819127 Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:42:24 -0400 Subject: [PATCH 1/2] feat(typescript-to-typebox): add `useEmitConstOnly` option --- src/typescript/typescript-to-typebox.ts | 34 +++++++++++++++++-------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/typescript/typescript-to-typebox.ts b/src/typescript/typescript-to-typebox.ts index 5a90de7..ddce17a 100644 --- a/src/typescript/typescript-to-typebox.ts +++ b/src/typescript/typescript-to-typebox.ts @@ -55,6 +55,11 @@ export interface TypeScriptToTypeBoxOptions { * for TypeBox which can operate on vanilla JS references. The default is false. */ useIdentifiers?: boolean + /** + * Specifies if the output code should include only `const` statements. In other words, + * the `type` declarations aren't emitted. The default is false. + */ + useEmitConstOnly?: boolean } /** Generates TypeBox types from TypeScript code */ export namespace TypeScriptToTypeBox { @@ -90,6 +95,8 @@ export namespace TypeScriptToTypeBox { let useIdentifiers = false // (option) specifies if typebox imports should be included let useTypeBoxImport = true + // (option) generate const statements only + let useEmitConstOnly = false // ------------------------------------------------------------------------------------------------------------ // AST Query // ------------------------------------------------------------------------------------------------------------ @@ -145,6 +152,12 @@ export namespace TypeScriptToTypeBox { return JsDoc.Parse(content) } // ------------------------------------------------------------------------------------------------------------ + // String Utilities + // ------------------------------------------------------------------------------------------------------------ + function JoinStatements(...statements: (string | false)[]): string { + return statements.filter(Boolean).join('\n') + } + // ------------------------------------------------------------------------------------------------------------ // Identifiers // ------------------------------------------------------------------------------------------------------------ function ResolveIdentifier(node: Ts.InterfaceDeclaration | Ts.TypeAliasDeclaration) { @@ -307,9 +320,9 @@ export namespace TypeScriptToTypeBox { const exports = IsExport(node) ? 'export ' : '' const members = node.members.map((member) => member.getText()).join(', ') const enumType = `${exports}enum Enum${node.name.getText()} { ${members} }` - const staticType = `${exports}type ${node.name.getText()} = Static` + const staticType = !useEmitConstOnly && `${exports}type ${node.name.getText()} = Static` const type = `${exports}const ${node.name.getText()} = Type.Enum(Enum${node.name.getText()})` - yield [enumType, '', staticType, type].join('\n') + yield JoinStatements(enumType, '', staticType, type) } function PropertiesFromTypeElementArray(members: Ts.NodeArray): string { const properties = members.filter((member) => !Ts.isIndexSignatureDeclaration(member)) @@ -342,23 +355,23 @@ export namespace TypeScriptToTypeBox { const parameters = node.typeParameters.map((param) => `${Collect(param)}: ${Collect(param)}`).join(', ') const members = PropertiesFromTypeElementArray(node.members) const names = node.typeParameters.map((param) => `${Collect(param)}`).join(', ') - const staticDeclaration = `${exports}type ${node.name.getText()}<${constraints}> = Static>>` + const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()}<${constraints}> = Static>>` const rawTypeExpression = IsRecursiveType(node) ? `Type.Recursive(This => Type.Object(${members}))` : `Type.Object(${members})` const typeExpression = heritage.length === 0 ? rawTypeExpression : `Type.Composite([${heritage.join(', ')}, ${rawTypeExpression}])` const type = InjectOptions(typeExpression, options) const typeDeclaration = `${exports}const ${node.name.getText()} = <${constraints}>(${parameters}) => ${type}` - yield `${staticDeclaration}\n${typeDeclaration}` + yield JoinStatements(staticDeclaration, typeDeclaration) } else { const exports = IsExport(node) ? 'export ' : '' const identifier = ResolveIdentifier(node) const options = useIdentifiers ? { ...ResolveOptions(node), $id: identifier } : { ...ResolveOptions(node) } const members = PropertiesFromTypeElementArray(node.members) - const staticDeclaration = `${exports}type ${node.name.getText()} = Static` + const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()} = Static` const rawTypeExpression = IsRecursiveType(node) ? `Type.Recursive(This => Type.Object(${members}))` : `Type.Object(${members})` const typeExpression = heritage.length === 0 ? rawTypeExpression : `Type.Composite([${heritage.join(', ')}, ${rawTypeExpression}])` const type = InjectOptions(typeExpression, options) const typeDeclaration = `${exports}const ${node.name.getText()} = ${type}` - yield `${staticDeclaration}\n${typeDeclaration}` + yield JoinStatements(staticDeclaration, typeDeclaration) } recursiveDeclaration = null } @@ -377,18 +390,18 @@ export namespace TypeScriptToTypeBox { const type_1 = isRecursiveType ? `Type.Recursive(This => ${type_0})` : type_0 const type_2 = InjectOptions(type_1, options) const names = node.typeParameters.map((param) => Collect(param)).join(', ') - const staticDeclaration = `${exports}type ${node.name.getText()}<${constraints}> = Static>>` + const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()}<${constraints}> = Static>>` const typeDeclaration = `${exports}const ${node.name.getText()} = <${constraints}>(${parameters}) => ${type_2}` - yield `${staticDeclaration}\n${typeDeclaration}` + yield JoinStatements(staticDeclaration, typeDeclaration) } else { const exports = IsExport(node) ? 'export ' : '' const options = useIdentifiers ? { $id: ResolveIdentifier(node), ...ResolveOptions(node) } : { ...ResolveOptions(node) } const type_0 = Collect(node.type) const type_1 = isRecursiveType ? `Type.Recursive(This => ${type_0})` : type_0 const type_2 = InjectOptions(type_1, options) - const staticDeclaration = `${exports}type ${node.name.getText()} = Static` + const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()} = Static` const typeDeclaration = `${exports}const ${node.name.getText()} = ${type_2}` - yield `${staticDeclaration}\n${typeDeclaration}` + yield JoinStatements(staticDeclaration, typeDeclaration) } recursiveDeclaration = null } @@ -591,6 +604,7 @@ export namespace TypeScriptToTypeBox { useIdentifiers = options?.useIdentifiers ?? false useTypeBoxImport = options?.useTypeBoxImport ?? true typenames.clear() + useEmitConstOnly = options?.useEmitConstOnly ?? false useImports = false useOptions = false useGenerics = false From 123e5c03a3d91673ce7c8866dcbccf94dd54f51a Mon Sep 17 00:00:00 2001 From: Alec Larson <1925840+aleclarson@users.noreply.github.com> Date: Fri, 4 Oct 2024 14:50:00 -0400 Subject: [PATCH 2/2] add test --- test/ts2typebox/index.ts | 49 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/test/ts2typebox/index.ts b/test/ts2typebox/index.ts index da59c96..abf11d2 100644 --- a/test/ts2typebox/index.ts +++ b/test/ts2typebox/index.ts @@ -576,4 +576,53 @@ describe('ts2typebox - Typescript to Typebox', () => { expectEqualIgnoreFormatting(generatedTypebox, expectedResult) }) }) + test('useEmitConstOnly', () => { + const generatedTypebox = TypeScriptToTypeBox.Generate( + ` + type A = { + a: number; + }; + type B = { + b: T; + }; + interface C { + c: string; + } + interface D { + d: T; + } + enum E { + e + } + `, + { useEmitConstOnly: true }, + ) + const expectedResult = ` + import { Type, Static, TSchema } from "@sinclair/typebox"; + + const A = Type.Object({ + a: Type.Number(), + }); + + const B = (T: T) => + Type.Object({ + b: T, + }); + + const C = Type.Object({ + c: Type.String(), + }); + + const D = (T: T) => + Type.Object({ + d: T, + }); + + enum EnumE { + e, + } + const E = Type.Enum(EnumE); + ` + expectEqualIgnoreFormatting(generatedTypebox, expectedResult) + }) })