badrap/valita

export type classes

dimatakoy opened this issue · 6 comments

After writing some mini libraries, I realized that we need to export the classes so we don't have to use type assetions on every line.

// import hierarhy that i suggest
import * as v from '@badrap/valita' // like right now
import * as valitaLib from '@badrap/valita/lib' // export classes for library authors here
jviide commented

What would the use case for this be, and what would valitaLib contain?

Related #29

What would the use case for this be, and what would valitaLib contain?

It should contain exports for ObjectType, UnionType, OptionalType, StringType and etc.

import { v } from './app/lib/core/schema-utils';

// I have 2-3 schemas for my entities: Domain, FormValidation/FormData, Database/ExternalApi

// domain entity
v.object({ id: v.number() });

// formValidation / FormData validation
// But this validation will fail if we pass data from api (schema above) as is.
v.object({
	id: v
		.string()
		.map(Number.parseInt)
		.assert((value) => value > 0),
});

// Lets you duplicate schema for working with incoming (form api) data too
v.object({
	id: v.union(
		v
			.string()
			.map(Number.parseInt)
			.assert((value) => value > 0),
		v.number(),
	),
});

// What if api can return null/undefined? I need to convert this to `''`:
v.object({
	id: v.union(
		v
			.string()
			.nullable()
			.chain((value) => {
				if (!value) return v.ok(Number.NaN); // pseudocode here, not covers all cases. numbers, booleans, unions is pretty funny for handling null/undefined/booleanish values 
				return v.ok(Number.parseInt(value)) ?? '';
			}),
		v.number(), // fallback to value from API
	),
});

// Finally, I decided to write a helper utility.
function field<T extends v.Type>(fieldType: T) {
	// pseudocode heres
	// returns v.union(inputType.chain((value) => fieldType.try(value)), fieldTyoe)
}

// We are here. I need a function that resolves original type and generates mapper from `inputType` to `fieldType if possible
// Resolves is a common function used in form libraries on the frontend (vee-validate, react-hook-forms, etc).

const fieldType = v.string().optional();
// ATM, I was forced to use constructions like, but I don't like it.
const resolveOriginalType = <T extends v.Type>(t: T) => (t.name === 'optional' ? t.type : null;

Probably the main and shortest point is that it will help users write smarter/safer utility functions or schema transformers.

Now, of course, it is also possible to write one and I did so. But inside the function there are a lot of @ts-expect-error and as any, which is pretty unsafe without additional testing.

types structure feels like implementation detail, but I need to rely on them. But because they're not exported, I feel like I'm relying on them and things can break.

If it's possible at the type level inside the library to fix this, that's ok too. In that case, I want to get some kind of union that would allow me to rely on name and x prop. With this solution I can avoid class exoption.

const SafeType = { name: 'optional'; type: Type } | { name: 'union'; options: Type[] } | {name: 'object', shape?: ... }

jviide commented

What I gather from this is that you want to define a type that's used internally in the system (the domain entity) and then use that to generate different input validators for different contexts (with typesafe type transformers).

If that is correct, then exposing the types wouldn't be enough in itself without some significant supporting type machinery (e.g. to ensure that v.number() is always transformed to something that produces a number etc.) In some previous discussions we had to rule these use-cases to be out of scope for this library, as it's not something we will ourselves use and thus can't commit to supporting.

Yes, you got me right. Just a few comments:

I've already written a library that works in my case. However, internally it relies on private and protected instance fields. This looks like a bit of a mess.

To be honest, the idea of making classes public doesn't seem so straightforward. The internals can still change.

What if we write a function that resolves the type and returns a type tree? Then you can keep the classes private, but users will be able to write their own transformers, and you as the library author will be able to change the implementation.

For example:

const name = v.string().nullable()

const tree = resolve(name)

console.log(tree)

// {name: 'string', nullable: true }

// or simpler, as types works
// {name: 'union', options: [ {name: 'null' }, {name: 'string'} ] }
jviide commented

In the example Valita would be used as a general data definition language (to define the domain entity types from which the input validators are generated) which we have outlined to be outside the scope of this library. I recommend skipping the validator -> JSON conversion phase, and start from the JSON ({name: 'union', options: [ {name: 'null' }, {name: 'string'} ] }) in your example) and generate the validators from that.