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

export type classes #49

Closed
dimatakoy opened this issue Aug 17, 2023 · 6 comments
Closed

export type classes #49

dimatakoy opened this issue Aug 17, 2023 · 6 comments

Comments

@dimatakoy
Copy link
Contributor

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
Copy link
Contributor

jviide commented Aug 17, 2023

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

@dimatakoy
Copy link
Contributor Author

dimatakoy commented Aug 17, 2023

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;

@dimatakoy
Copy link
Contributor Author

dimatakoy commented Aug 17, 2023

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
Copy link
Contributor

jviide commented Aug 17, 2023

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.

@dimatakoy
Copy link
Contributor Author

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
Copy link
Contributor

jviide commented Aug 18, 2023

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.

@jviide jviide closed this as completed Aug 18, 2023
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

2 participants