Skip to content

Commit

Permalink
add type inference
Browse files Browse the repository at this point in the history
  • Loading branch information
franklevasseur committed Feb 9, 2024
1 parent 00b44d4 commit e202826
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 10 deletions.
2 changes: 1 addition & 1 deletion jex/src/jex-representation/jex-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const $ = {
null: () => ({ type: 'null' }),
undefined: () => ({ type: 'undefined' }),
literal: _literal,
object: <const Args extends Record<string, types.JexType>>(properties: Args) => ({ type: 'object', properties }),
object: <Args extends Record<string, types.JexType>>(properties: Args) => ({ type: 'object', properties }),
array: <const Args extends types.JexType>(items: Args) => ({ type: 'array', items: items as Args }),
map: <const Args extends types.JexType>(items: Args) => ({ type: 'map', items: items as Args }),
tuple: <const Args extends types.JexType[]>(items: Args) => ({ type: 'tuple', items }),
Expand Down
31 changes: 30 additions & 1 deletion jex/src/jex-representation/jex-extends.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as utils from '../utils'
import { JexInfer } from './jex-infer'
import * as types from './typings'
import { jexExtends } from './jex-extends'
import { expect, test } from 'vitest'
Expand Down Expand Up @@ -38,6 +40,9 @@ test('jex-extends should be true if child and parent are the same', () => {
test('jex-extends should be true if child is an object with more properties than parent', () => {
const child = $.object({ a: $.string(), b: $.number() })
const parent = $.object({ a: $.string() })
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

Expand All @@ -59,6 +64,9 @@ test('jex-extends should be false if an optional property of child is required i
test('jex-extends should be true if when child is a union with less types than parent', () => {
const child = $.union([$.string(), $.number()])
const parent = $.union([$.string(), $.number(), $.undefined(), $.null()])
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

Expand All @@ -73,6 +81,9 @@ test('jex-extends should be false if child is a union with more types than paren
test('jex-extends should be true if child is a literal with same base type than parent', () => {
const child = $.literal('banana')
const parent = $.union([$.string(), $.number()])
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

Expand All @@ -87,6 +98,9 @@ test('jex-extends should be false if child is a primitive and parent is a litera
test('jex-extends should be true if child is a literal included in parent union', () => {
const child = $.literal('banana')
const parent = $.union([$.string(), $.number(), $.literal('banana')])
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

Expand All @@ -101,6 +115,9 @@ test('jex-extends should be false if child is a literal not included in parent u
test('jex-extends should be true if child and parents are arrays and child items extends parent items', () => {
const child = $.array($.string())
const parent = $.array($.union([$.string(), $.undefined()]))
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

Expand All @@ -111,14 +128,20 @@ test('jex-extends should be false if child and parents are arrays and child item
expectJex(child).not.toExtend(parent)
})

// [string, string] extends string[], [string, number] extends 🔥 (string | number)[]
// [string, string] extends string[], [string, number] extends (string | number)[]
test('jex-extends should be true if child is a tuple and parent is an array with the same items', () => {
const tupleStrStr = $.tuple([$.string(), $.string()])
const arrayStr = $.array($.string())
type _childExtendsParent1 = utils.types.Expect<
utils.types.Extends<JexInfer<typeof tupleStrStr>, JexInfer<typeof arrayStr>>
>
expectJex(tupleStrStr).toExtend(arrayStr)

const tupleStrNum = $.tuple([$.string(), $.number()])
const arrayStrNum = $.array($.union([$.string(), $.number()]))
type _childExtendsParent2 = utils.types.Expect<
utils.types.Extends<JexInfer<typeof tupleStrNum>, JexInfer<typeof arrayStrNum>>
>
expectJex(tupleStrNum).toExtend(arrayStrNum)
})

Expand All @@ -133,13 +156,19 @@ test('jex-extends should be false if child is a tuple and parent is an array wit
test('jex-extends should be true if child and parent are objects and child items extends parent items', () => {
const child = $.object({ a: $.string() })
const parent = $.object({ a: $.union([$.string(), $.number()]) })
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

// Record<string, string> extends Record<string, string | undefined | boolean>
test('jex-extends should be true if child and parents are maps and child items extends parent items', () => {
const child = $.map($.string())
const parent = $.map($.union([$.string(), $.undefined(), $.boolean()]))
type _child = JexInfer<typeof child>
type _parent = JexInfer<typeof parent>
type _childExtendsParent = utils.types.Expect<utils.types.Extends<_child, _parent>>
expectJex(child).toExtend(parent)
})

Expand Down
41 changes: 41 additions & 0 deletions jex/src/jex-representation/jex-infer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as utils from '../utils'
import * as types from './typings'

type _InferTuple<T extends types.JexType[]> = T extends []
? []
: T extends [infer A]
? [JexInfer<utils.types.Cast<A, types.JexType>>]
: T extends [infer A, ...infer B]
? [JexInfer<utils.types.Cast<A, types.JexType>>, ..._InferTuple<utils.types.Cast<B, types.JexType[]>>]
: T

export type JexInfer<J extends types.JexType> =
J extends types.JexStringLiteral<infer V>
? V
: J extends types.JexNumberLiteral<infer V>
? V
: J extends types.JexBooleanLiteral<infer V>
? V
: J extends types.JexString
? string
: J extends types.JexNumber
? number
: J extends types.JexBoolean
? boolean
: J extends types.JexNull
? null
: J extends types.JexUndefined
? undefined
: J extends types.JexUnion
? JexInfer<J['anyOf'][number]>
: J extends types.JexObject
? { [K in keyof J['properties']]: JexInfer<J['properties'][K]> }
: J extends types.JexArray
? JexInfer<J['items']>[]
: J extends types.JexMap
? { [key: string]: JexInfer<J['items']> }
: J extends types.JexAny
? any
: J extends types.JexTuple
? _InferTuple<J['items']>
: never
36 changes: 36 additions & 0 deletions jex/src/jex-representation/jex-infer.types.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import * as utils from '../utils'
import { $ } from './jex-builder'
import { JexInfer } from './jex-infer'

const _any = $.any()
const _string = $.string()
const _number = $.number()
const _boolean = $.boolean()
const _null = $.null()
const _undefined = $.undefined()
const _literalStr = $.literal('apple')
const _literalNum = $.literal(1)
const _literalBool = $.literal(true)
const _object = $.object({ a: $.string(), b: $.number() })
const _array = $.array($.string())
const _map = $.map($.string())
const _tuple = $.tuple([$.string(), $.number()])
const _union = $.union([$.string(), $.number()])
const _complex = $.array($.union([$.map($.tuple([$.string(), $.number()])), $.object({ a: $.array($.string()) })]))

type ExpectedComplex = (Record<string, [string, number]> | { a: string[] })[]
type _Any = utils.types.Expect<utils.types.Equals<JexInfer<typeof _any>, any>>
type _String = utils.types.Expect<utils.types.Equals<JexInfer<typeof _string>, string>>
type _Number = utils.types.Expect<utils.types.Equals<JexInfer<typeof _number>, number>>
type _Boolean = utils.types.Expect<utils.types.Equals<JexInfer<typeof _boolean>, boolean>>
type _Null = utils.types.Expect<utils.types.Equals<JexInfer<typeof _null>, null>>
type _Undefined = utils.types.Expect<utils.types.Equals<JexInfer<typeof _undefined>, undefined>>
type _LiteralStr = utils.types.Expect<utils.types.Equals<JexInfer<typeof _literalStr>, 'apple'>>
type _LiteralNum = utils.types.Expect<utils.types.Equals<JexInfer<typeof _literalNum>, 1>>
type _LiteralBool = utils.types.Expect<utils.types.Equals<JexInfer<typeof _literalBool>, true>>
type _Object = utils.types.Expect<utils.types.Equals<JexInfer<typeof _object>, { a: string; b: number }>>
type _Array = utils.types.Expect<utils.types.Equals<JexInfer<typeof _array>, string[]>>
type _Map = utils.types.Expect<utils.types.Equals<JexInfer<typeof _map>, Record<string, string>>>
type _Tuple = utils.types.Expect<utils.types.Equals<JexInfer<typeof _tuple>, [string, number]>>
type _Union = utils.types.Expect<utils.types.Equals<JexInfer<typeof _union>, string | number>>
type _Complex = utils.types.Expect<utils.types.Equals<JexInfer<typeof _complex>, ExpectedComplex>>
7 changes: 2 additions & 5 deletions jex/src/jex-representation/typings.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
/**
* Internal representation of Jex Schema
* Simpler than JSON Schema and capable of representing all TypeScript types
*/
import * as utils from '../utils'

export type JexStringContent = string
export type JexString = {
Expand Down Expand Up @@ -71,7 +68,7 @@ export type JexAny = {

export type JexTuple = {
type: 'tuple'
items: JexType[]
items: utils.types.Tuple<JexType, number>
}

export type JexType =
Expand Down
2 changes: 1 addition & 1 deletion jex/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export * as collection from './collection-utils'
export * as typings from './type-utils'
export * as types from './type-utils'
8 changes: 7 additions & 1 deletion jex/src/utils/type-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ export type Resolve<T> = T extends (...args: infer A) => infer R
export type Cast<T, U> = T extends U ? T : U

type _Tuple<T, N extends number, R extends any[]> = R['length'] extends N ? R : _Tuple<T, N, [T, ...R]>
export type Tuple<T, N extends number> = number extends N ? N[] : _Tuple<T, N, []>
export type Tuple<T, N extends number> = number extends N ? T[] : _Tuple<T, N, []>

export type Expect<T extends true> = T extends true ? true : 'Expectation failed'

export type Extends<T, U> = T extends U ? true : false

export type Equals<T, U> = Extends<T, U> extends true ? Extends<U, T> : false
2 changes: 1 addition & 1 deletion jex/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ import { configDefaults, defineConfig } from 'vitest/config'

export default defineConfig({
test: {
exclude: [...configDefaults.exclude, '**/*.utils.test.ts']
exclude: [...configDefaults.exclude, '**/*.utils.test.ts', '**/*.types.test.ts']
}
})

0 comments on commit e202826

Please sign in to comment.