Io-ts: Usage of static interface when defining a runtime interface

Created on 9 Jan 2019  路  6Comments  路  Source: gcanti/io-ts

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?

discussion

Most helpful comment

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

All 6 comments

@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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

philipp-schaerer-lambdait picture philipp-schaerer-lambdait  路  3Comments

loxs picture loxs  路  7Comments

jakejx picture jakejx  路  7Comments

spacejack picture spacejack  路  3Comments

cloudify picture cloudify  路  4Comments