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

[Feature] Input & output type infer #4

Open
Shuunen opened this issue Mar 2, 2023 · 2 comments
Open

[Feature] Input & output type infer #4

Shuunen opened this issue Mar 2, 2023 · 2 comments

Comments

@Shuunen
Copy link

Shuunen commented Mar 2, 2023

Given the following snippet :

const person = object({
    name: string().or(() => 'john'),
    age: number(),
  });

type Person = Infer<typeof person>;

Here Person is :

type Person = {
    name: string;
    age: number;
}

This is a correctly inferred type but it's an output type, because it's the type of the object returned by the person function.

Let's say we have a method that post a user to an API that expect a Person object :

function createPerson(input: unknown) {
    const newPerson = person(input);
    axios.post('/api/person', newPerson); // send a Person object
}

It's weird to see unknown as input in a TypeScript project, here we could write code like createPerson(12) and TS would not complain.

Here we cannot type the input parameter via input: Person because the name property is not optional.

But name should be optional because it has a default value.

Here we see that the same schema has a different input and an output type.

What it can take as input is not the same as what it actually return.

We could imagine a InferInput type that would return the input type of a schema.

type PersonInput = InferInput<typeof person>;

function createPerson(input: PersonInput) {
    const newPerson = person(input);
    axios.post('/api/person', newPerson); // send a Person object
}

Now we have a proper type for the input parameter.

This feature has been implemented in Zod :

const stringToNumber = z.string().transform((val) => val.length);

type input = z.input<typeof stringToNumber>; // string
type output = z.output<typeof stringToNumber>; // number
@thoughtspile
Copy link
Owner

Ah yes, I am playing with this idea. It enables quite a few data transformation scenarios, from one with a default value that you suggested to exhausive type normalization:

array(string())
  .or(string().map(s => [s]))
  .or(set(string()).map(s => [...s]))

Besides, I suppose it would help us to make proper union types with objectLoose({ x: string() }).or(objectLoose({ y: string })), where objectLoose could be generic WRT the input of the or.

I'm not yet sure how to implement it, or what the most convenient API would be for declaring narrow input types, so let's have it sit here for a while.

@jordan-boyer
Copy link
Contributor

jordan-boyer commented Jun 20, 2023

We finally solved this problem with another approach.

import { object, number, string, optional, Banditype } from 'banditypes'

type Simplify<T> = T extends Object ? { [K in keyof T]: T[K] } : T;

const personSchema = object({
    name: string().or(optional()),
    age: number(),
    profession: string().or(optional()),
})

function withDefault<Schema extends object, Inputs extends Schema, Defaults extends Partial<Schema>> (
    schema: Banditype<Schema>,
    inputs: Inputs,
    defaults: Defaults & { [K in keyof Defaults]: K extends keyof Schema ? Schema[K] : never },
  ) {
    const data = schema(inputs);
    const result = {
        ...defaults,
        ...data,
    }
    return result as Simplify<typeof result>;
}


const input = { age: 12 }
const person1 = withDefault(personSchema, input, { profession: 'John Doe' });
const person2 = withDefault(personSchema, input, { name: 'John Doe', foobar: 'azerty' }); // 1
const person3 = withDefault(personSchema, input, { name: 'John Doe' });

function showName(name: string) { console.log(name) }

showName(person1.name) // 2
showName(person2.name) // 3
showName(person3.name) // 4
  1. An error because foobar is not a key of personSchema
  2. An error because name was not provided in input nor in defaults
  3. No error because name was provided in defaults
  4. Same as 3

We hope this can help anyone which want to achieve this using banditypes =)

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

3 participants