Skip to content
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
33 changes: 33 additions & 0 deletions src/type-system/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -619,6 +619,38 @@ export const ElysiaType = {
return value
})
.Encode((value) => value) as any as TUnsafe<Uint8Array>
},

/**
* Wraps an external TypeBox schema to make it compatible with Elysia's type system.
*
* This is useful when using schemas from packages that use a different TypeBox instance,
* such as `drizzle-typebox`, where TypeScript's unique symbol for `Kind` causes
* type incompatibility even though the schemas are structurally identical.
*
* @example
* ```typescript
* import { createSelectSchema } from 'drizzle-typebox'
* import { t, Elysia } from 'elysia'
*
* const userTable = pgTable('users', { id: integer('id'), name: text('name') })
* const UserSchema = createSelectSchema(userTable)
*
* // Wrap the external schema to make it compatible
* const app = new Elysia()
* .get('/users', () => [], {
* response: t.Array(t.External(UserSchema))
* })
* ```
*
* @param schema - An external TypeBox schema
* @returns The schema cast to Elysia's TSchema type
* @see https://github.com/elysiajs/elysia/issues/1688
*/
External: <T extends { static: unknown; params: unknown[] }>(
schema: T
): TUnsafe<T['static']> => {
return schema as unknown as TUnsafe<T['static']>
}
}

Expand Down Expand Up @@ -670,6 +702,7 @@ t.Form = ElysiaType.Form

t.ArrayBuffer = ElysiaType.ArrayBuffer
t.Uint8Array = ElysiaType.Uint8Array as any
t.External = ElysiaType.External

export { t }

Expand Down
138 changes: 138 additions & 0 deletions test/type-system/external.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, it } from 'bun:test'
import { Elysia, t } from '../../src'
import { Kind, TObject, TString, Type } from '@sinclair/typebox'

/**
* Tests for t.External() helper which allows external TypeBox schemas
* (e.g., from drizzle-typebox) to be used with Elysia's type system.
*
* @see https://github.com/elysiajs/elysia/issues/1688
*/
describe('t.External', () => {
it('should accept external TypeBox schema', () => {
// Simulate an external TypeBox schema (like from drizzle-typebox)
// by creating a schema with the same structure but potentially
// from a different module
const externalSchema = Type.Object({
id: Type.Number(),
name: Type.String()
})

// t.External should wrap the external schema
const wrappedSchema = t.External(externalSchema)

// Verify the schema properties are preserved
expect(wrappedSchema.type).toBe('object')
expect(wrappedSchema.properties).toBeDefined()
expect(wrappedSchema.properties!.id.type).toBe('number')
expect(wrappedSchema.properties!.name.type).toBe('string')
})

it('should preserve static type inference', () => {
const externalSchema = Type.Object({
id: Type.Number(),
name: Type.String()
})

const wrappedSchema = t.External(externalSchema)

// Type inference should work
type InferredType = typeof wrappedSchema.static
const _typeCheck: InferredType = { id: 1, name: 'test' }

expect(true).toBe(true)
})

it('should work with t.Array', () => {
const externalSchema = Type.Object({
id: Type.Number(),
name: Type.String()
})

// This is the main use case from issue #1688
const arraySchema = t.Array(t.External(externalSchema))

expect(arraySchema.type).toBe('array')
expect(arraySchema.items).toBeDefined()
})

it('should work in route response schema', async () => {
const externalSchema = Type.Object({
id: Type.Number(),
name: Type.String()
})

const app = new Elysia().get('/users', () => [{ id: 1, name: 'test' }], {
response: t.Array(t.External(externalSchema))
})

const response = await app.handle(new Request('http://localhost/users'))
const data = await response.json()

expect(response.status).toBe(200)
expect(data).toEqual([{ id: 1, name: 'test' }])
})

it('should validate response correctly', async () => {
const externalSchema = Type.Object({
id: Type.Number(),
name: Type.String()
})

const app = new Elysia().get(
'/invalid',
// Return invalid data
() => [{ id: 'not-a-number', name: 123 }] as any,
{
response: t.Array(t.External(externalSchema))
}
)

const response = await app.handle(new Request('http://localhost/invalid'))

// Should fail validation
expect(response.status).toBe(422)
})

it('should work with nested objects', () => {
const addressSchema = Type.Object({
street: Type.String(),
city: Type.String()
})

const userSchema = Type.Object({
id: Type.Number(),
name: Type.String(),
address: addressSchema
})

const wrappedSchema = t.External(userSchema)

expect(wrappedSchema.type).toBe('object')
expect(wrappedSchema.properties!.address).toBeDefined()
})

it('should work with schemas containing Kind symbol', () => {
// Create a schema that has the Kind symbol (like real TypeBox schemas)
const externalSchema: TObject<{ name: TString }> = {
[Kind]: 'Object',
type: 'object',
properties: {
name: {
[Kind]: 'String',
type: 'string',
static: '',
params: []
} as TString
},
required: ['name'],
static: { name: '' },
params: []
} as any

// t.External should handle schemas with Kind symbol
const wrappedSchema = t.External(externalSchema)

expect(wrappedSchema.type).toBe('object')
})
})
Loading