I've just knocked this up in an attempt to come at the problem of "unreadable" types:
export type StaticInterface = Record<string, any>;
export type RuntimeTypeOf<S extends StaticInterface> = { [k in keyof S]: t.Type<S[k], any> };
export const interfaceFromRuntimeType = <S extends StaticInterface>(
props: RuntimeTypeOf<S>,
name?: string,
) => {
const inner = t.interface(props, name);
const outer = new t.InterfaceType<RuntimeTypeOf<S>, S>(
inner.name,
inner.is,
inner.validate,
inner.encode,
inner.props,
);
return outer;
};
// definition of class elided:
// class Iso8601DateStringType extends t.Type<Date, string>
export const DateRange = interfaceFromRuntimeType<{
EndTime: Date;
StartTime: Date;
}>({
EndTime: Iso8601DateString,
StartTime: Iso8601DateString,
});
export type DateRange = t.TypeOf<typeof DateRange>;
Because the static type is stored as the _A, you get simple types in code like:
DateRange.decode(blob).map(r => {
// r is { EndTime: Date; StartTime: Date; } here
})
Thoughts?
@leemhenson the output type looks wrong
export const DateRange = interfaceFromRuntimeType({
EndTime: Iso8601DateString,
StartTime: Iso8601DateString
})
/*
const DateRange: t.InterfaceType<RuntimeTypeOf<{
EndTime: Date;
StartTime: Date;
}>, {
EndTime: Date;
StartTime: Date;
}, {
EndTime: Date; <= should be string
StartTime: Date; <= should be string
}, unknown>
*/
Also looks like there's the usual issue with nested types (try with [email protected])
export const Nested = interfaceFromRuntimeType({
a: interfaceFromRuntimeType({
b: t.string
})
})
/*
const Nested: t.InterfaceType<RuntimeTypeOf<{
a: {
b: any;
};
}>, {
a: {
b: any;
};
}, {
a: {
b: any;
};
}, unknown>
*/
Actually the types are unreadable bacause of this last issue, as a workaround I'm using type aliases like TypeOfProps which don't produce anys (although they make the types unreadable)
BUT...
fortunately [email protected] doesn't have this issue anymore!
I'm working on [email protected] (branch 1.6) and we'll finally get decent types.
Example
[email protected] (current stable)
const Person = t.type({
name: t.string,
age: t.number
})
/*
const Person: t.InterfaceType<{
name: t.StringType;
age: t.NumberType;
}, t.TypeOfProps<{
name: t.StringType;
age: t.NumberType;
}>, t.OutputOfProps<{
name: t.StringType;
age: t.NumberType;
}>, unknown>
*/
branch 1.6
const Person = t.type({
name: t.string,
age: t.number
})
/*
const Person: t.InterfaceType<{
name: t.StringType;
age: t.NumberType;
}, {
name: string;
age: number;
}, {
name: string;
age: number;
}, unknown>
*/
Ah, magic. I'll sit on my hands until 1.6 is born!
We can reduce the boilerplate even further if we introduce an intermediary interface between t.type and InterfaceType
export class InterfaceType<P, A = any, O = A, I = unknown> extends Type<A, O, I> {
...
}
// this is new
export interface InterfaceT<P extends Props>
extends InterfaceType<P, { [K in keyof P]: TypeOf<P[K]> }, { [K in keyof P]: OutputOf<P[K]> }, unknown> {}
// returns the new interface ------------------------------------------------------------v
export const type = <P extends Props>(props: P, name: string = getNameFromProps(props)): InterfaceT<P> => {
...
}
Result
const Person = t.type({
name: t.string,
age: t.number,
nested: t.type({
foo: t.boolean
})
})
/*
const Person: t.TypeC<{
name: t.StringC;
age: t.NumberC;
nested: t.TypeC<{
foo: t.BooleanC;
}>;
}>
*/
type Person = t.TypeOf<typeof Person>
/*
type Person = {
name: string;
age: number;
nested: {
foo: boolean;
};
}
*/
Person.decode({}).map(p => p.age /* p is { name: string, age: number, nested: { foo: boolean } } here */)
I added such intermediary interfaces for all the runtime types and the test declaration file went from a whopping 11,908 LOCs (current stable 1.5.2) to 136 LOCs (branch 1.6)
Btw you can install the preview version by running npm i gcanti/io-ts#1.6-lib if you want to try it out.
Any feedback would be greatly appreciated!
/cc @gabejohnson
@leemhenson the intermediary interfaces can be ported to 1.5.x (i.e. no [email protected] requirement)
I wrote a candidate patch release (1.5.3), need some help to make sure is backward compatible,
if you have the time you can try it out running npm i gcanti/io-ts#1.5.3-lib
EDIT: I checked io-ts-types and fp-ts-routing (which use io-ts) and they look ok
I've changed jobs so I no longer have access to the large project that was using a lot of io-ts types. I've got a small project here and the proposed 1.5.3 branch looks good on here, but it's very basic usage.
Most helpful comment
We can reduce the boilerplate even further if we introduce an intermediary interface between
t.typeandInterfaceTypeResult
I added such intermediary interfaces for all the runtime types and the test declaration file went from a whopping 11,908 LOCs (current stable
1.5.2) to 136 LOCs (branch1.6)Btw you can install the preview version by running
npm i gcanti/io-ts#1.6-libif you want to try it out.Any feedback would be greatly appreciated!
/cc @gabejohnson