Io-ts: Restore the O type parameter to Encoder

Created on 17 May 2020  路  9Comments  路  Source: gcanti/io-ts

馃殌 Feature request

Current Behavior

Encoder<A>

gives

encode: (a: A) => unknown

Desired Behavior

Encoder<A, B>

giving

encode: (a: A) => B

I have been using io-ts for encoding from private data types to request bodies, where the unknown type is fine, but also to things like graphql schemas where the destination typed, e.g.:

interface GraphqlProduct {
  __typename: "Product";
  name: string;
  // ... etc
}

Now that Encoder always returns unknown I have to cast, which is obviously not ideal.

I started working through the functions in Encoder.ts but I'm stumped on what to do for:

export function type<A>(properties: { [K in keyof A]: Encoder<A[K]> }): Encoder<A>

I suppose there would be a subsequent impact to the schema types too. Perhaps if you decide to derive an Encoder instance, you have the option to provide an output type for each schema element?

experimental

Most helpful comment

use JsonEncoder in Codec

I'd propose JsonCodec (hardcoded Json output type) vs Codec (reusing generic Encoder) instead

All 9 comments

Does it mean there is no Output type anymore in v3?

@gcanti @leemhenson what about something like this?

export interface Encoder<A, B> {
  readonly encode: (a: A) => B
}

export type TypeOf<E> = E extends Encoder<infer A, any> ? A : never

export type OutputOf<E> = E extends Encoder<any, infer B> ? B : never

// ...

export function type<Props extends Record<string, Encoder<any, any>>>(
  properties: Props
): Encoder<{ [K in keyof Props]: TypeOf<Props[K]> }, { [K in keyof Props]: OutputOf<Props[K]> }> {
  // runtime implementation same as before
}

It seems to work ok when used directly:

/** simplified version of the existing `export const encoder: Contravariant<URI> & Schemable<URI>` */
const e = {
  string: id as Encoder<string, string>,
  number: id as Encoder<number, number>,
  type,
  array,

  /** an asymmetric encoder, similar to io-ts-type's `DateFromISOString` */
  dateFromString: {
    encode: date => date.toISOString()
  } as Encoder<Date, string>,
}

const Person = e.type({
  name: e.string,
  age: e.number,
  dob: e.dateFromString,
  orders: e.array(e.type({
    id: e.string,
    date: e.dateFromString
  }))
})

// types look correct:
import { expectTypeOf } from 'expect-type'

expectTypeOf<TypeOf<typeof Person>>().toEqualTypeOf<{
  name: string;
  age: number;
  dob: Date;
  orders: Array<{ id: string; date: Date }>
}>()

expectTypeOf<OutputOf<typeof Person>>().toEqualTypeOf<{
  name: string;
  age: number;
  dob: string;
  orders: Array<{ id: string; date: string }>
}>()

expectTypeOf(Person.encode).returns.toEqualTypeOf<OutputOf<typeof Person>>()

The only trouble I had was with HKT/Schemable's assumptions about what the type combinator looks like. It doesn't allow for it to have an extends clause in the generic, or to return a type with two typeargs rather than one. By not using the Schemable interface, it works as expected.

By not using the Schemable interface, it works as expected

@mmkal @leemhenson if we don't use Schemable we can implement a PEncoder data type

export interface PEncoder<O, A> {
  readonly encode: (a: A) => O
}

and then derive Encoder<A> = PEncoder<unknown, A> from it, see https://github.com/gcanti/io-ts/blob/028392215b00d02840586fedf668f8fbe709d7e2/src/PEncoder.ts.

@leemhenson would PEncoder serve your use case?

I have been using io-ts for encoding from private data types to request bodies

Actually we could derive from PEncoder any O-fixed encoder, for example a JsonEncoder = PEncoder<Json, A>

export interface JsonArray extends Array<Json> {}
export type JsonObject = { [key: string]: Json }
export type Json = null | string | number | boolean | JsonObject | JsonArray

export interface JsonEncoder<A> {
  readonly encode: (a: A) => Json
}

@mlegenhausen there's no v3 in the works, everything I was working on in the last few months has been backported to v2.2.x (as experimental modules)

@mmkal @leemhenson thinking twice, since the main goal of decoders / encoders is to handle JSON payloads, JsonEncoder seems more useful than the current Encoder which outputs unknown, so I would propose:

  • rename Encoder to JsonEncoder and change its fixed output type to Json
  • use JsonEncoder in Codec
  • rename PEncoder to Encoder

what do you think?

use JsonEncoder in Codec

I'd propose JsonCodec (hardcoded Json output type) vs Codec (reusing generic Encoder) instead

Yeah this way around makes much more sense to me, and I would imagine to folks migrating up from earlier versions.

Any chance we could get this and the rest of the upcoming 2.2.3 changes published as a io-ts@next so I can try them out?

Closed by #471

Was this page helpful?
0 / 5 - 0 ratings