npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

io-interface

v1.10.2

Published

Generate io-ts codec solution from TypeScript native interface

Readme

io-interface

io-interface auto generates runtime validation solutions from TypeScript native interfaces. It's main purpose is to validate JSON data from a web API but you can use it to validate any external data.

// src/app/models/user.ts
// Step 1. define an interface
export interface User {
  name: string;
  title?: string;
  age: number;
  recentOrders: Order[];
}

// src/app/services/decoder.service.ts
// Step 2. add `schema<User>()` to the manifest to register
import { schema, Decoder } from 'io-interface';

export const decoder = new Decoder([schema<User>()]);

// src/app/users/user.service.ts
// Step 3. use `decode()` and `decodeArray()` to do the validation/conversion.
const user: User | undefined = decoder.decode<User>('User', json, console.error);
const users: User[] | undefined = decoder.decodeArray<User>('User', json, console.error);

Motivation

Validating data coming from an external system is always good. Image you found a powerful runtime validation library io-ts and want to adopt it to your project, but the concern is all the team members have to learn this new library and understand how it works. This would slow down the developing pace. And in many cases we don't want this slowdown.

So here comes the encapsulation. The goal is the rest of the team need to learn nearly nothing to use this facility and the minimum code changes are required to adopt it. For other developers they can still simply use a native TypeScript interface to represent the data model from web API. And use one-liner to auto-generate the validation solution.

You can check out this Angular repo as a demo project.

Limitations

Since its main purpose is for JSON validation, only a subset of interface syntax is supported here. The fields of an interface must of type:

  1. Primitive types: number, string, boolean
  2. Other acceptable interfaces
  3. Classes
  4. Literal types (i.e. interface Address { pos: { lat: number; lng: number; } }, here Address.pos is a literal type)
  5. Union types
  6. null type
  7. Array type of 1-5

Also

  1. The fields in the interface CAN be marked as optional.
  2. any, unknown are illegal.
  3. Recursive types are NOT supported.
  4. Intersection types are NOT supported YET.
  5. Generic types supporting are experimental and for now you need to manually create factory method for it.

You need declare schemas in topological order

Right now there's no dependency resolver implemented. So for example you have these interfaces:

interface LatLng {
  lat: number;
  lng: number;
}

interface Order {
  price: number;
  pos: LatLng;
}

You must declare LatLng's schema before Order:

const schemas = [
  //...
  schema<LatLng>(), // MUST come first
  schema<Order>(),
];

But don't worry too much about this, if you declare them in a wrong order, you will receive a error from the library.

Assign casters to classes

It's very often we're passing Date in JSON, and Date is a class instead of an interface in TypeScript.

interface Order {
  date: Date;
}

We have to manually create a caster for a class. Luckily the decoder for Date is already implemented in io-ts-types. What we need to do is to just incluce into the 2nd argument.

import { DateFromISOString } from 'io-ts-types/lib/DateFromISOString';

const decoder = new Decoder([schema<Order>()], {
  Date: DateFromISOString,
});

It's equivalent to:

const decoder = new Decoder();
decoder.casters.Date = DateFromISOString;
decoder.register(schema<Order>());

Builtin types

In types.ts you can found some common types and its casters:

/** @since 1.7.3 */
export const casters = {
  Date: DateFromISOString,
  Int: t.Int,
  Latitude: Latitude,
  Longitude: Longitude,
  NonEmptyString: NonEmptyString,
};

Usage:

import { casters } from 'io-interface/types';

const dec = new Decoder(
  [
    /* schemas */
  ],
  casters,
);

Enum types

You can easily register an enum using enumSchema

import { enumSchema } from 'io-interface';

enum Status {
  Pending = 'pending',
  Complete = 'complete',
}

decoder.register(enumSchema('Status', Status));
const status = decoder.decode<Status>('Status', 'pending');

Extending the interface

You may subclass Model<T> to extend the interface to a class:

import { Model } from 'io-interface';

interface IUser {
  firstName: string;
  lastName: string;
}

interface User extends IUser {}
class User extends Model<IUser> { // IMPORTANT!!! You need Model<T> to get the constructor
  get name(): string {
    return `${user.firstName} ${user.lastName}`;
  },
}

decoder.register({
  schema: schema<IUser>(),
  constructor: User,
  className: 'User',
});

const user = decoder.decode<User>('User', { firstName: 'Yang', lastName: 'Liu' });

console.log(user.name);

Installation

Setup ts-patch

  1. npm install -D ts-patch

  2. add "postinstall" script to package.json to auto-patch the compiler after npm install

    {
      "scripts": {
        "postinstall": "ts-patch install"
      }
    }
  3. npm install -D io-interface

  4. add transformer to tsconfig.json

    {
      "compilerOptions": {
        "plugins": [{ "transform": "io-interface/transform-interface" }]
      }
    }

To verify the setup, try compile this file.

import { schema } from 'io-interface';

interface Order {
  price: number;
  date: Date;
  note?: string;
  pricelines: number[];
}
const OrderSchema = schema<Order>();
console.log(OrderSchema);

You should see the console message like this:

image

[Angular] A DecoderService

The example code is as follows.

import { Injectable } from '@angular/core';
import { Decoder, schema } from 'io-interface';
import { casters } from 'io-interface/types';
import { BadTodo } from '../models/bad-todo';
import { Todo } from '../models/todo';

@Injectable({
  providedIn: 'root',
})
export class DecoderService {
  readonly schemas = [schema<Todo>(), schema<BadTodo>()];

  readonly dec = new Decoder(this.schemas, casters);

  decode<T>(typeName: string, data: unknown): T | undefined {
    return this.dec.decode<T>(typeName, data, console.error);
  }

  decodeArray<T>(typeName: string, data: unknown): T[] | undefined {
    return this.dec.decodeArray<T>(typeName, data, console.error);
  }
}

Just replace console.error with a real error handler in your project.

Daily usage

1. define an interface

// src/app/models/todo.ts
export interface Todo {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

2. register the type to DecoderService's schemas

// src/app/services/decoder.service.ts
readonly schemas = [schema<Todo>()];

3. Use DecoderService to convert the data

// src/app/services/todo.service.ts
  getTodo(): Observable<Todo> {
    return this.http.get('https://jsonplaceholder.typicode.com/todos/1').pipe(
      map(json => this.dec.decode<Todo>('Todo', json)),
      filter(todo => !!todo),
    );
  }

[Optional] can we DRY it more?

As you can see from the signature decode<Todo>('Todo', json), Todo repeats twice. But for native TypeScript this is needed because the type parameter is for static environment and method parameter is for runtime environment. I don't find a very good solution here but I created a specific TypeScript transformer to expand a macro such as decode<Todo>(json) to decode<Todo>('Todo', json). Since TypeScript will never populate the interface information to runtime so I guess this would be the easiest way to reduce the duplication.

Because I didn't find any decent macro system for TypeScript so this macro implementation is very specific and not configurable. It replaces:

requestAndCast<User>(...args);

To:

request(...args, (decoder, data, onError) => decoder.decode('User', data, onError));

So if you want use this ensure you declares such methods.

Installation

To enable this, install transform-request to tsconfig plugins:

{
  "compilerOptions": {
    "plugins": [
      { "transform": "io-interface/transform-interface" },
      { "transform": "io-interface/transform-request" } // <--- add this
    ]
  }
}

And here's an example implementation.

type DecoderCallback<T> = (
  c: Decoder,
  data: unknown,
  onError: (e: string[]) => void,
) => T | undefined;

class ApiService {
  // use it in your codebase
  async requestAndCast<T>(options: ApiOptions): T {
    throw new Error(`macro failed to expand,
    check your tsconfig and ensure "io-interface/transform-request" is enabled`);
  }

  // do not call it directly, transformer will call it
  async request<T>(
    options: ApiOptions,
    cb: (c: Decoder, data: unknown, e?: DecoderCallback<T>) => T | undefined,
  ) {
    const data: Object = await fetch(options);
    const casted: T = cb(c, data, console.error);
    return casted;
  }
}

Error handling

If encoding failed, decode() or decodeArray() will can an onError callback with signature: string[] => void where the argument is an array of error messages. Here's the screenshot of such error messages:

image