Splitting apart Schemable interface
🚀 Feature request
Current Behavior
First of all, thanks for all your hard work in the fp-ts ecosystem!
I've been playing around with Schemable stuff, and having used it for a bit, I was curious about the possibility and merit of breaking the Schemable/Schemable1/Schemable2C interfaces into individual interfaces for each property, kind of following the direction of the "extensions" like WithUnknownContainer/WithUnion/etc. that exist now. I have a branch where I was playing around with this idea here https://github.com/gcanti/io-ts/compare/master...andywhite37:schemable-split - I'm not intending to pull-request this branch, it was more just experimental. I also didn't do a complete splitting up of the interface, I just extracted out 6 or so of the properties to see where it would go.
Basically, in the end it might look like:
// Base interface to carry the URI
interface Schemable<S> {
readonly URI: S
}
interface WithString<S> {
readonly string: HKT<S, string>
}
interface WithNumber<S> {
readonly string: HKT<S, number>
}
// and so on for all parts of current `Schemable`
Desired Behavior
Right now, the Schemable<S> interface includes a variety of "base" type properties that cover the most common use cases for creating schemas. The upside of this grouping is that you can more easily create schemas with less boilerplate in constructing more specific combinations of types. You can also mix in the With* extension interfaces if you want to add things like the UnknownContainers, etc.
I think the downside of this grouping is that it makes it harder to control exactly which types you want to deal with in your schemas - like if you use the current Schemable<S>, you get access to any of the parts of it for creating schemas, including things that you might not actually want to deal with, like maybe nullable or partial. The other downside with Schemable<S> as it stands now is that you also have to make all of your instances of Schemable<S> support all of the given types, even if you explicitly don't want to use them in your schemas. Finally, with the current setup, there is some additional boilerplate needed if you want to use Schemable<S> with one of the extensions like WithUnknownContainer, as you have to create a new Schema<A> interface, possibly new make and interpreter functions, if you want to follow that convention.
Suggested Solution
Break apart the Schemable interfaces into a set of interfaces with one "type" property per interface. Maybe there is a base Schemable<S> with just the URI, and then you & intersect the parts you want, like WithString<S>, WithNumber<S>, etc.
The fallout of this change would be:
- User gets full control over the types they want to use for the
Schemableinterface they want to create- User can limit which types they want to support in schemas
- User can control which types they have to support in the instances of
Schemablesthey will use with theirSchemas - The
Schemable<S>instances can still be a big combination of all possible implementations, but the schemas can be more selective about what they actually need
- Because
Schema<S>is just a function(S: Schemable<S>) => HKT<S, A>, I wonder if this could be generalized somehow so you don't need aSchema<S>interface for each combination ofSchemable<S>s you create - maybe it could somehow just be the function without the wrapper interface - Maybe the
makeandinterpreterfunctions could also be more generalized or even eliminated to reduce boilerplate when creating new combinations ofSchemableinterfaces
Who does this impact? Who is this for?
Current users of Schemable. The current Schemable<S> interface, Schema<A>, make, and interpreter functions could be preserved as just a specialized combination of the parts they currently contain, so the change wouldn't have to be breaking, but would allow for more flexibility and modularity going forward. The change might also lead to more general solutions for things like Schema<A>, make, interpreter, etc.
Your environment
| Software | Version(s) |
|---|---|
| io-ts | 2.2.14 |
that cover the most common use cases for creating schemas
Yeah, that's the rationale behind the current Schemable interface :+1:
I'm not against a fine grained set of interfaces, however I don't think it would make much of a difference in practice, you can easily choose the operations you want to use using Pick:
import { HKT, Kind, Kind2, URIS, URIS2 } from 'fp-ts/HKT'
import * as S from 'io-ts/Schemable'
type With = 'URI' | 'string' | 'number' | 'boolean' | 'type'
export interface MySchemable<S> extends Pick<S.Schemable<S>, With> {}
export interface MySchemable1<S extends URIS> extends Pick<S.Schemable1<S>, With> {}
export interface MySchemable2C<S extends URIS2, E> extends Pick<S.Schemable2C<S, E>, With> {}
export interface MySchema<A> {
<S>(S: MySchemable<S>): HKT<S, A>
}
export function make<A>(schema: MySchema<A>): MySchema<A> {
return S.memoize(schema)
}
export type TypeOf<S> = S extends MySchema<infer A> ? A : never
export function interpreter<S extends URIS2>(
S: MySchemable2C<S, unknown>
): <A>(schema: MySchema<A>) => Kind2<S, unknown, A>
export function interpreter<S extends URIS>(S: MySchemable1<S>): <A>(schema: MySchema<A>) => Kind<S, A>
export function interpreter<S>(S: MySchemable<S>): <A>(schema: MySchema<A>) => HKT<S, A> {
return (schema) => schema(S)
}
export const person = make((S) => S.type({ name: S.string, person: S.number }))
wonder if this could be generalized somehow Maybe the make and interpreter functions could also be more generalized
It would be nice for sure, unfortunately I haven't found a way to do that :(
That With approach is a nice way of breaking it up using the existing types.
I agree that splitting it up wouldn't make much of a difference without being able to generalize or eliminate the Schema/make/interpreter functions that go with each combination - that's where most of the duplication/boilerplate comes in. I played around with a few ideas too, but didn't land on anything remarkable.