Skip to content

feat(typescript-to-typebox): add useEmitConstOnly option #53

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 24 additions & 10 deletions src/typescript/typescript-to-typebox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
// ------------------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<typeof ${node.name.getText()}>`
const staticType = !useEmitConstOnly && `${exports}type ${node.name.getText()} = Static<typeof ${node.name.getText()}>`
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<Ts.TypeElement>): string {
const properties = members.filter((member) => !Ts.isIndexSignatureDeclaration(member))
Expand Down Expand Up @@ -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<ReturnType<typeof ${node.name.getText()}<${names}>>>`
const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()}<${constraints}> = Static<ReturnType<typeof ${node.name.getText()}<${names}>>>`
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<typeof ${node.name.getText()}>`
const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()} = Static<typeof ${node.name.getText()}>`
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
}
Expand All @@ -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<ReturnType<typeof ${node.name.getText()}<${names}>>>`
const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()}<${constraints}> = Static<ReturnType<typeof ${node.name.getText()}<${names}>>>`
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<typeof ${node.name.getText()}>`
const staticDeclaration = !useEmitConstOnly && `${exports}type ${node.name.getText()} = Static<typeof ${node.name.getText()}>`
const typeDeclaration = `${exports}const ${node.name.getText()} = ${type_2}`
yield `${staticDeclaration}\n${typeDeclaration}`
yield JoinStatements(staticDeclaration, typeDeclaration)
}
recursiveDeclaration = null
}
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions test/ts2typebox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -576,4 +576,53 @@ describe('ts2typebox - Typescript to Typebox', () => {
expectEqualIgnoreFormatting(generatedTypebox, expectedResult)
})
})
test('useEmitConstOnly', () => {
const generatedTypebox = TypeScriptToTypeBox.Generate(
`
type A = {
a: number;
};
type B<T> = {
b: T;
};
interface C {
c: string;
}
interface D<T> {
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 extends TSchema>(T: T) =>
Type.Object({
b: T,
});

const C = Type.Object({
c: Type.String(),
});

const D = <T extends TSchema>(T: T) =>
Type.Object({
d: T,
});

enum EnumE {
e,
}
const E = Type.Enum(EnumE);
`
expectEqualIgnoreFormatting(generatedTypebox, expectedResult)
})
})