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 🙏

© 2024 – Pkg Stats / Ryan Hefner

io-ts-extended

v1.0.1-beta.3

Published

Run-time testing if io-ts types extend one another, with some extras.

Downloads

325

Readme

io-ts-extended

This package extends the io-ts package with functionality to test if one type extends another, like the extends keyword in TypeScript, alongside a few types and other utilities.

Additionally, you can add extension testing logic for your own types.

The run-time logic is tested against TypeScript's compile-time logic for all test cases.

Installation

npm install io-ts-extended

Usage

Testing two types:

import * as t from 'io-ts-extended';

const type1 = t.type({ a: t.string });
const type2 = t.type({ a: t.string, b: t.number });

const type2ExtendsType1 = t.isExtensionOf(type2, type1); // true
const type1ExtendsType2 = t.isExtensionOf(type1, type2); // false

Extending the extension testing logic:

Example

import * as t from 'io-ts-extended';

class MyType extends t.Type<..> { ... }

t.extensionRegistry.register(
	MyType, // source type
	t.UnionType, // target type
	t.unionTargetDefaultHandler, // default handler for union target types
	undefined, // no intersection handler
);

// repeat for every type class that can be extended by MyType as well as any
// type class that can extend MyType

Signature

The signature of the extension registration function is:

t.extensionRegistry.register = <S extends t.TypeCtor, T extends t.TypeCtor>(
	source: S,
	target: T,
	test: (
		source: t.InstanceType<S>,
		target: t.InstanceType<T>,
		isExtendedBy: t.IsExtendedBy
	) => Ternary,
	intersectionHandler: InstanceType<T> extends t.DictionaryType<t.Any, t.Any>
		? t.DictionaryIntersectionHandler<S, T>
		: InstanceType<T> extends t.InterfaceType<any>
		? t.InterfaceIntersectionHandler<S, T>
		: InstanceType<T> extends t.PartialType<any>
		? t.PartialIntersectionHandler<S, T>
		: undefined,
): void;

Here t.TypeCtor is any type class that extends t.Type<any, any, any>. The parameters are described below.

Parameters

source

A source type class that can extend the target type class.

target

A target type class that can be extended by the source type class.

test

The test parameter requires a function that tests if the source type extends the target type, returning t.Ternary.True if it does, t.Ternary.False if it does not and t.Ternary.Maybe if it cannot be ruled out nor confirmed (for recursive types). Its third parameter is a function that should be used to test if a type extends another type, as it tracks the types that have already been tested to avoid infinite loops. Note that, in contrast to isExtensionOf it takes the target as the first argument, and the source as the second argument.

intersectionHandler

The intersectionHandler is required, but undefined for most types. It is defined if the target is a t.DictionaryType, t.InterfaceType, or t.PartialType. It requires some type-specific logic to handle the case of testing for an intersection source containing an instance of source.

DictionaryIntersectionHandler (when the target is dictionary-like)

The signature of DictionaryIntersectionHandler is:

type DictionaryIntersectionHandler<S extends TypeCtor, T extends TypeCtor> = (
	source: InstanceType<S>,
	target: InstanceType<T>,
	isExtendedBy: IsExtendedBy,
	setDictSourceResult: SetDictSourceResult,
	setPropsSourceResult: SetPropsSourceResult,
	addIntersectionMember: (type: t.Type<unknown>) => void,
) => void;

The first three parameters correspond to that of the test function.

The setDictSourceResult is a function that should be called in case the source type is a dictionary-like type (i.e. it constrains every possible key to a specific type). It must be passed the result of testing the source type against the target type.

The setPropsSourceResult is a function that should be called in case the source type is an interface-like type (i.e. it constraints some but not all keys). It must be passed a callback with the following signature:

(codomain: t.Type<unknown>, key: string) => Ternary;

It must return the result of comparing the type at the given key of the source against the target dictionary's codomain.

The addIntersectionMember function can be used to add a type to the source intersection, used in cases such as of a wrapped type like a t.ExactType.

InterfaceIntersectionHandler (when the target is interface-like)

The signature of InterfaceIntersectionHandler is:

type InterfaceIntersectionHandler<S extends TypeCtor, T extends TypeCtor> = (
	source: InstanceType<S>,
	targetKey: string | number,
	targetType: InstanceType<T>,
	isExtendedBy: IsExtendedBy,
	hasExtendingProp: HasExtendingProp,
) => Ternary;

The source and isExtendedBy parameters correspond to that of the test function.

The targetKey parameter is the key of the target interface that is being tested for compatibility with the source interface, targetType is its type.

hasExtendingProp is a function with the following signature:

(
	source: t.Type<unknown>,
	targetKey: string | number,
	targetType: t.Type<unknown>,
) => Ternary

It should be used to test if some source type has a property that extends the target type at the given key.

PartialIntersectionHandler (when the target is partial-like)

The signature of PartialIntersectionHandler is:

type PartialIntersectionHandler<S extends TypeCtor, T extends TypeCtor> = (
	source: InstanceType<S>,
	targetKey: string | number,
	target: InstanceType<T>,
	isExtendedBy: IsExtendedBy,
	hasPartiallyExtendingProp: HasPartiallyExtendingProp,
) => Ternary | undefined;

It is basically identical to that of the InterfaceIntersectionHandler, except that it must return undefined if the source type does not have the property at the given key.

Extra types

This package also provides a few extra types that are not present in io-ts; t.FunctionType, t.ClssType, t.NullableType and t.PromiseType. They come with their some quirks.

t.FunctionType

t.FunctionType can be used to specify functions with a specific signature, allowing one to define parameter names and types, as well as the return type. As such it carries more information than io-ts's native version.

It doesn't make sense to me to serialize functions. Therefore, using the default instantiator t.fn(), encoding returns a special null function, and decoding will return an optional implementation or the null function. There is also a helper function t.stripNullFunctions() that can be used to recursively remove all null functions from an object.

Example:

const myFn = t.fn(
	[
		['param1', t.string],
		['param2', t.number],
	] as const,
	t.boolean
);

t.ClssType

t.ClssType (instantiable with t.clss()) allows tying a static and instance interface to a t.Implementation, in order to build a type whose type and implementation can be easily extended. To extend a t.ClssType, you can use the t.extendClss() helper.

Methods are removed during encoding, and reinstated during decoding.

Example:

const MyClss = t.clss(
	'MyClss',
	t.type({
		staticProp: t.string,
		staticMethod: t.fn([['a', t.string]] as const, t.boolean),
	}),
	t.type({
		instanceProp: t.boolean,
		instanceMethod: t.fn([['a', t.number]] as const, t.boolean),
	}),
	class extends t.Implementation {
		static staticProp: string = 'foo';
		static staticMethod(a: string): boolean { return true; };
		instanceProp: boolean = false;
		instanceMethod(a: number): boolean { return true; };
	},
);

t.NullableType

t.NullableType wraps its type argument in a union that includes t.null and t.undefined.

t.PromiseType

t.PromiseType allows defining promises with some caveats: validation will succeed for all promises, whereas decoding will always successfully return a promise, throwing a t.PromiseTypeError if the value does not correspond to the type.

The t.decode() helper, returning a promise, can be used to decode a promise, resolving with the decoded value if it is of the correct type, or rejecting if it is not. By supplying rejection logic the decoding of promises can be handled in a more controlled manner.

Other utilities

t.render()

Every type prototype has been extended with a render method that returns a string representation of the type. t.render() will call this method on its argument.

t.decode()

A function that takes a type and a value and returns a promise that resolves with the decoded value if it is of the correct type, or rejects with the t.Errors object if it is not.

t.ternarySome() and t.ternaryEvery()

These are the ternary counterparts of Array.prototype.some() and Array.prototype.every().

Notes

Tests are driven by TypeScript itself

TypeScipt is used as the ground truth for the extension testing logic; i.e. the run-time result of this package is compared to the compile-time result of TypeScript for all tests.

This is achieved by leveraging the TypeScript compiler API; after extracting the source and target types for all test cases, a source file is created that contains TypeScript extension test statements for each pair of types. TypeScript's result is then looked up and compared to the outcome of the run-time logic.

In addition, test cases contain an expectation of the outcome. It does not influence the result, but it's helpful for comparing TypeScript's result against your own expectations.

strictFunctionTypes set to false

The package was built having compilerOptions.strictFunctionTypes set to false in tsconfig.json. The application of many functions provided by this package will fail to compile if this is not the case.

Every type should be uniquely named

Some types take a name as an argument, and it is important that every type is uniquely named. The reason is that during the extension testing process, the source and target name are used to avoid infinite loops. If two types have the same name, the extension testing logic may give an incorrect result.

Use a build tool

This package is distributed as a typed CommonJs module, which should be no problem when using TypeScript. For use in a browser, use a build tool like vite to transpile and bundle.

Some types to watch out for

A record extends a partial whose properties are incompatible. I have no clue what the logic behind this is.

type source = Record<string, unknown>;
type target = Partial<{ a: string, b: number }>;
const test: source extends target ? true : false = true;

Unlike for object types, for tuple types the intersection of two tuples is not the tuple of the intersections of each tuple element type.

type source = [unknown, number];
type target = [string, unknown];
const test: source extends target ? true : false = false;

Static properties are not checked when comparing classes.

class A {
	static a: string = 'foo';
}

class B extends A {
	static b: number = 42;
}

const test: B extends A ? true : false = true;

In TypeScript, numeric keys are not assignable to numeric string keys, yet JavaScript never returns a key as a number. It is therefore not possible to require strictly numeric or non-numeric keys.

type source = 1;
type target = keyof { '1': unknown };
const test: source extends target ? true : false = false;

Apparently any may or may not extend Readonly<any>.

type source = any;
type target = Readonly<any>;
const test: source extends target ? true : false = true;
const test: source extends target ? true : false = false;

While all other types (except any) extend their readonly counterpart, for unknown this is not the case.

type source = unknown;
type target = Readonly<unknown>;
const test: source extends target ? true : false = false;

Recursive function parameters (like you will ever use them) are also curious:

type source1 = (a: string) => boolean;
type source2 = (a: string, b: (a: string) => boolean) => boolean;
type target = (a: string, b: target) => boolean;

const test1: source1 extends target ? true : false = true;
const test2: source2 extends target ? true : false = false;
const test3: target extends target ? true : false = true;