Skip to content
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

Generate readonly arrays and corresponding union types for enum types #864

Open
kamilogorek opened this issue Jan 7, 2025 · 0 comments
Open

Comments

@kamilogorek
Copy link
Member

Currently our enums are generated like so:

export type Database = {
  public: {
    Enums: {
      api_key_type: 'FOO' | 'BAR' | 'BAZ'
    }
  } 
}

This however makes it impossible to be used in all kinds of parsers or validators, which require a value as an input, not the type, as those are stripped during compilation.

A solution to this is ti generate a type for every corresponding enum in a form of:

export const status = ['FOO', 'BAR', 'BAZ'] as const
export type status = (typeof status)[number]

and somehow expose it publicly.

Our existing `ts-morph` implementation below (click to expand)
// This script uses ts-morph to generate a "runtime enums", which are
// simply an array of all possible union types.
// The values are based on generated database enums and are required
// in order for us to be able to use those types in all kind of validators.
// It also creates additional copy of the same union type, with the same name
// and based on the array readonly values for convinience.

import { Project, SyntaxKind, VariableDeclarationKind } from 'ts-morph'

const inputFilePath = process.argv[2]
const enumsFilePath = process.argv[3]

if (!inputFilePath) {
  throw new Error(`generate-runtime-enums.ts requires input path to be specified as 1st argument`)
}

if (!enumsFilePath) {
  throw new Error(`generate-runtime-enums.ts requires output path to be specified as 2nd argument`)
}

console.log(`> Generating runtime enums from ${inputFilePath} at ${enumsFilePath}...`)

const project = new Project()
const originalFile = project.addSourceFileAtPath(inputFilePath)
const outputFile = project.createSourceFile(enumsFilePath, '', { overwrite: true })

const generatedEnums = originalFile
  .getTypeAliasOrThrow('Database')
  .getTypeNodeOrThrow()
  .getFirstDescendant(
    (node) => node.isKind(SyntaxKind.PropertySignature) && node.getName() === 'public'
  )
  ?.getFirstDescendant(
    (node) => node.isKind(SyntaxKind.PropertySignature) && node.getName() === 'Enums'
  )
  ?.getDescendantsOfKind(SyntaxKind.PropertySignature)

if (!generatedEnums) {
  throw new Error(
    `No enums found, this should never happen; Tell Kamil he messed up and should fix it.`
  )
}

for (const enumProp of generatedEnums) {
  const name = enumProp.getName()
  const values = enumProp
    .getTypeNodeOrThrow()
    .getType()
    .getUnionTypes()
    .map((type) => type.getLiteralValue())
    .filter((value) => typeof value === 'string')

  outputFile.addVariableStatement({
    declarationKind: VariableDeclarationKind.Const,
    declarations: [
      {
        name,
        initializer: `[${values.map((value) => `'${value}'`).join(', ')}] as const`,
      },
    ],
    isExported: true,
  })

  outputFile.addTypeAlias({
    name,
    type: `(typeof ${name})[number]`,
    isExported: true,
  })
}

outputFile.saveSync()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant