Skip to content

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

@kamilogorek

Description

@kamilogorek

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()

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions