Skip to content

Conversation

alpinagyok
Copy link

@alpinagyok alpinagyok commented Sep 12, 2025

Overview

This PR adds support for parsing custom schema extension with parserExtensions option. This is not based on any concrete issue but just on my personal problem that I'm trying to solve. For context:

Problem

We use @sinclair/typebox + json-schema-to-typescript to share types between services and we have some non-standard schemas. Specifically, we commonly use typebox'es "Javascript constructs" like Type.Function.

Let's say I have this

import { Type } from '@sinclair/typebox';

const Test = Type.Function([Type.String()], Type.Number())

/*
which is just this JSON ⬇️
{
  type: 'Function',
  parameters: [
    {
      type: 'string',
    },
  ],
  returns: {
    type: 'number',
  },
}
*/

In this compile will just default to CUSTOM_TYPE which makes sense.

export interface Test {
  [k: string]: unknown;
}

Currently we hack around this issue with tsType like so

const Test = {
	...Type.Function([Type.String()], Type.Number()),
	tsType: '(someArg: string) => number',
}

which gets us the correct type.

export type Test = ((someArg: string) => number) => number;

While this, works you can already start to feel that we're doing some duplicated work which might be very error prone (Type.String and Type.Number are already handled by json-schema-to-typescript). Add one more level to the schema and tsType just becomes not worth it for us

const Test = Type.Function([
	Type.Object({
		id: Type.String(),
		name: Type.String(),
	})
], Type.Number());

/*
which is just this JSON ⬇️
{
  type: 'Function',
  parameters: [
    {
      type: 'object',
      properties: {
        id: {type: 'number'},
        name: {type: 'string'},
      },
      required: ['id'],
    },
  ],
  returns: {
    type: 'number',
  },
}
*/

Solution

Added parserExtensions option that allows defining a custom compile callback for unsupported type. When type matches, it runs the callback. Callback provides the current schema that's being parsed and compileSchema callback to basically pass the parsing back to the library.

Simple example

const Test = { type: 'myCustomString' };

compile(Test as any, 'test', {
  bannerComment: '',
  format: true,
  parserExtensions: {
    myCustomString: () => 'string',
  },
})

results in

export type Test = string;

Complicated example (with the mentioned Type.Function)

const Test = {
  type: 'Function',
  parameters: [
    {
      type: 'object',
      properties: {
        id: {type: 'number'},
        name: {type: 'string'},
      },
      required: ['id'],
    },
  ],
  returns: {
    type: 'number',
  },
}

compile(Test as any, 'test', {
  bannerComment: '',
  format: true,
  parserExtensions: {
    Function: (schema: any, compileSchema) => {
      const paramTypes = schema.parameters
        ? schema.parameters.map((param: any, index: number) => {
            const paramType = compileSchema(param)
            return `param${index}: ${paramType}`
          })
        : []

      const returnType = schema.returns ? compileSchema(schema.returns) : 'void'

      return `(${paramTypes.join(', ')}) => ${returnType}`
    },
  },
})

results in

export type Test = (param0: {id: number; name?: string; [k: string]: unknown}) => number;

Side Note

I'm well aware that I need to add tests. Willing to do that add and fix any concerns. Want to validate first if the idea is even welcome though 🙏, @bcherny.

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

Successfully merging this pull request may close these issues.

1 participant