Io-ts: Decode Class Instance, Validate Methods

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

Hello, I'm trying to validate class instances for the existence of methods and I'm seeing that decode doesn't see the instance methods (likely because they aren't enumerable). I want to do something like this:

var t = require("io-ts")

class Foo {
  constructor() {
    this.something = 'lskdfj'
  }
  bar () {
    return null
  }
}

// I've tried `t.interface` as well
const FooT = t.type({
  something: t.string,
  bar: t.Function,
})

FooT.decode(new Foo()).fold((e) => {
  console.log('error')
  console.log(e)
}, (thing) => {
  console.log('success')
})

You can see this running here:
https://runkit.com/rjhilgefort/5c32e52179a07a0012303212

Is there anyway to do what I'm trying to do with io-ts or any suggestion of how I could do it? Worth mentioning that TS is not a consideration here. This piece of code is still plain old JS. Thanks in advance for the help!

Most helpful comment

If I understand this correctly, I see two possibly different concerns that are mixed here:
a) deserialise at the IO a JSON value and instantiate a class out of it
b) validate that certain values are actually instanceof a specific class, inside the rest of the codebase (non IO)

for a), it should be pretty straightforward using a custom codec. See e.g. my answer here

for b), a few considerations:

  • this isn't a use case io-ts has been designed for: as the name suggests, it should be used at the IO boundary, not "inside" the app business code
  • the usage from plain JS means loosing many of the benefits of the library. b) is something you'd never want/need in a TS codebase

This being said, I think for b) a codec along the lines of this could work:

interface Newable {
  new (...args: any[]): any;
}

function instanceOf<C extends Newable>(C: C): t.Type<InstanceType<C>> {
  return new t.Type<InstanceType<C>>(
    `instanceOf(${C.name})`,
    (v): v is InstanceType<C> => v instanceof C,
    (i, c) =>
      i instanceof C ? t.success<InstanceType<C>>(i) : t.failure(i, c),
    v => v
  );
}

usage:

class A {
  constructor(readonly foo: string) {}
  getFoo(): string {
    return this.foo;
  }
}

const a = new A("foo");
console.log(instanceOf(A).decode(a)); // Right
console.log(instanceOf(A).decode({})); // Left
console.log(instanceOf(A).decode({ foo: "foo" })); // Left
console.log(instanceOf(A).decode(new Object())); // Left
// and the following type checks:
console.log(
  pipe(
    instanceOf(A).decode(a),
    fold(() => "woops", a => a.getFoo())
  )
);

All 6 comments

I'm not thrilled with the solution, but I updated t.type to support class instances in this PR on my fork: https://github.com/rjhilgefort/io-ts/pull/1 and https://github.com/rjhilgefort/io-ts/pull/2

I think there a couple better solutions:

  • Optimize the code I put in by having t.type respect the new keys code (that I have commented out in my branch).
  • Create a new type t.instance that does pretty much the same thing as t.type but will grab the non-enumerable keys on the instance.

I would love some advice and/or help on better ways to do this that would be acceptable to make a PR back to io-ts. I don't think the code I have on my fork is ready for a PR as it stands.

Isn't it possible to create a custom codec for this? It comes with cons though. Something like:

import * as t from "io-ts";
import {Type} from "io-ts";

export interface InstanceOfType<T extends Function> extends Type<T, T, unknown> {}

export const instanceOf = <T extends Function>(constructor: T, name: string): InstanceOfType<T> =>
  new Type<T, T, unknown>(
    name,
    (u): u is T => u instanceof constructor,
    (u, c) => u instanceof constructor ? t.success(u as T) : t.failure(u, c),
    (p) => p,
  );

EDIT: To answer my own question, no this won't play with nicely in a single stroke with validate, encode, and decode because you get a typeof T.

@rockymadden thanks for the response and attempt. This is still an issue for the project I used this on so I'm still looking for solutions.

@rjhilgefort You bet. I was working with a lib that has this same dilemma (ended up solving a diff way). The above works but once you validate, encode, or decode you get a typeof T which has no info about instance members (i.e. no really, this encoded thing really does have a foo method).

You can get around this by using unknown in place of T but that is nasty and you have to then as T against those unknowns all over (eww). There might be a way to allow peeps to describe instances first via an io-ts interface/type that will be used where T is (i.e. tell me what this instance will contain). But how to constrain the constructor to T? So that the constructor will ensure instanceof (that the constructor is in the prototype chain) will work while also ensuring it will build an instance as described (or vice versa)? If you could solve, the nice thing is that as a codec no fork is needed.

One thing to note is that instanceOf was once in this lib, very much like above, in v0.0.3.

After looking at the source, I think you could use a custom codec to do this. You might notice if you use partial things will work in the example (it only validates something). If we look more closely at type and partial we see the usage of Object.keys and hasOwnProperty. So, if we use instance friendly operations like in instead, it might be possible to do via a codec and not a fork.

const MyInstanceC = instance({
  something: t.string,
  bar: t.Function,
});

If I understand this correctly, I see two possibly different concerns that are mixed here:
a) deserialise at the IO a JSON value and instantiate a class out of it
b) validate that certain values are actually instanceof a specific class, inside the rest of the codebase (non IO)

for a), it should be pretty straightforward using a custom codec. See e.g. my answer here

for b), a few considerations:

  • this isn't a use case io-ts has been designed for: as the name suggests, it should be used at the IO boundary, not "inside" the app business code
  • the usage from plain JS means loosing many of the benefits of the library. b) is something you'd never want/need in a TS codebase

This being said, I think for b) a codec along the lines of this could work:

interface Newable {
  new (...args: any[]): any;
}

function instanceOf<C extends Newable>(C: C): t.Type<InstanceType<C>> {
  return new t.Type<InstanceType<C>>(
    `instanceOf(${C.name})`,
    (v): v is InstanceType<C> => v instanceof C,
    (i, c) =>
      i instanceof C ? t.success<InstanceType<C>>(i) : t.failure(i, c),
    v => v
  );
}

usage:

class A {
  constructor(readonly foo: string) {}
  getFoo(): string {
    return this.foo;
  }
}

const a = new A("foo");
console.log(instanceOf(A).decode(a)); // Right
console.log(instanceOf(A).decode({})); // Left
console.log(instanceOf(A).decode({ foo: "foo" })); // Left
console.log(instanceOf(A).decode(new Object())); // Left
// and the following type checks:
console.log(
  pipe(
    instanceOf(A).decode(a),
    fold(() => "woops", a => a.getFoo())
  )
);
Was this page helpful?
0 / 5 - 0 ratings

Related issues

xogeny picture xogeny  路  5Comments

mottetm picture mottetm  路  5Comments

ggoodman picture ggoodman  路  8Comments

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

abaco picture abaco  路  10Comments