Motivation: refinements are inherently unsafe
Example
const PositiveInt = t.refinement(t.Integer, n => n > 0, 'PositiveInt')
const Person = t.type({
name: t.string,
age: PositiveInt
})
type Person = t.TypeOf<typeof Person>
const person: Person = {
name: 'name',
age: -1.2 // <= no errors
}
I propose to deprecate refinement in favour of a brand combinator
Definition
declare const _brand: unique symbol
export interface Brand<B> {
readonly [_brand]: B
}
export interface BrandC<C extends t.Any, B>
extends t.RefinementType<C, t.TypeOf<C> & Brand<B>, t.OutputOf<C>, t.InputOf<C>> {}
// replaces refinement
export const brand = <C extends t.Any, B extends string>(
codec: C,
predicate: Predicate<t.TypeOf<C>>,
name: B
): BrandC<C, B> => {
return t.refinement(codec, predicate, name)
}
// replaces Integer
export const Int = brand(number, n => n % 1 === 0, 'Int')
Usage
// const Int: BrandC<t.NumberC, "Int">
const Int = brand(t.number, n => n % 1 === 0, 'Int')
// type Int = number & Brand<"Int">
type Int = t.TypeOf<typeof Int>
const Positive = brand(t.number, n => n > 0, 'Positive')
// type Positive = number & Brand<"Positive">
type Positive = t.TypeOf<typeof Positive>
const PositiveInt = t.intersection([Int, Positive])
// type PositiveInt = number & Brand<'Int'> & Brand<'Positive'>
type PositiveInt = t.TypeOf<typeof PositiveInt>
import { PathReporter } from 'io-ts/lib/PathReporter'
console.log(PathReporter.report(PositiveInt.decode(1.1)))
// [ 'Invalid value 1.1 supplied to : (Int & Positive)/0: Int' ]
console.log(PathReporter.report(PositiveInt.decode(-1)))
// [ 'Invalid value -1 supplied to : (Int & Positive)/1: Positive' ]
const Person = t.type({
name: t.string,
age: PositiveInt
})
type Person = t.TypeOf<typeof Person>
const p: Person = {
name: 'Giulio',
age: -1.2 // Type 'number' is not assignable to type 'Brand<"Int">
}
The brand signature is almost identical to refinement (the name argument is required though).
The migration path should be straightforward and incremental: you can switch to brand, one refinement at the time, and fix the possible type-checking errors that could happen.
Yeah "ad-hoc branding" is something I'm doing a lot now, pending https://github.com/Microsoft/TypeScript/issues/202 resulting in something less boilerplatey, like type PositiveInt = unique number. I've been casting to the brand in my io-ts types, so this combinator feels pretty natural to me.
Yeah "ad-hoc branding" is something I'm doing a lot now
@leemhenson that's great, your early feedback would be much appreciated, you can try the release candidate by running npm i gcanti/io-ts#1.8.0-lib
Works as expected and saves me a bit of boilerplate! 馃憤
I'm using refined types a lot, but had to implement own type and combinator:
import { AnyNewtype, Newtype } from "newtype-ts"
import * as t from "io-ts"
export type Refined<_URI extends { readonly [K in symbol | string | number]: symbol }, A> =
A & Newtype<_URI, A>
export function refine<T extends AnyNewtype, RT extends t.Type<T["_A"]>>(
rt: RT,
isT: (val: t.OutputOf<RT>) => val is T,
name: string
): t.Type<T, T, t.InputOf<RT>> {
return new t.Type(
name,
(val: t.InputOf<RT>): val is T => rt.is(val) && isT(val),
(val, ctx) => rt.decode(val).filterOrElseL(isT, _ => [t.getValidationError(_, ctx)]),
t.identity
)
}
But the issue with this is that it's _legal_ to access the _URI property and then compiler will not scream unique symbol field name fixes this and is also shorter to write type definitions.
It may be also useful to have a Refined (or Branded) type exported by io-ts so it can be used to declare types explicitely*, ex:
export type Refined<A, Tag extends string> = A & Brand<Tag>
type Int = t.Refined<number, "Int">
_*I'm often declaring types explicitly rather than extracting with t.TypeOf, to avoid 'uge definition files (I know it was improved in latest versions: https://github.com/gcanti/io-ts/issues/165)_
Hope to see this functionality in next minor release 馃憤.
It may be also useful to have a Refined (or Branded) type exported by io-ts so it can be used to declare types explicitely
@lostintime what's the use case? Let's say we define Positive
const Positive = t.brand(t.number, n => n > 0, 'Positive')
type Positive = t.TypeOf<typeof Positive>
/*
same as
type Positive = number & t.Brand<"Positive">
*/
How would you use a Branded type?
export type Branded<A, B> = A & Brand<B>
@gcanti as I mentioned - I'm currently defining types explicitly, then adding decoders for them, to get cleaner error messages, and less verbose definition files, ex:
type UserId = t.Branded<string, "UserId">
const UserId: t.Type<UserId> = t.brand(t.number, _ => true, "UserId")
type User = {
userId: UserId
name: string
}
const User: t.Type<User> = t.interface({
userId: UserId,
name: t.string
})
Maybe that's not the case for t.brand (or it depends on parent type verbosity), but in order to keep code style consistent I think I'd be using same for refined too.
My approach have some other pitfalls also - for types with optional values - you may forget to add some fields to decoders and compiler will not complain :(, that's why I was searching long time for a way to pass explicit types parameters to decoders also, ex:
const User: t.Type<User> = t.interface<User>({...})
but it doesn't work this way, full decoder type is expected.
For example this definition:
export const Person = t.interface({
name: t.intersection([
t.interface({
firstName: t.string,
lastName: t.string
}),
t.partial({
middleName: t.string
})
]),
cityName: t.string
})
export const MyPersons = t.interface({
something: t.string,
persons: t.array(Person)
})
generates this definition (using [email protected]):
export declare const Person: t.TypeC<{
name: t.IntersectionC<[t.TypeC<{
firstName: t.StringC;
lastName: t.StringC;
}>, t.PartialC<{
middleName: t.StringC;
}>]>;
cityName: t.StringC;
}>;
export declare const MyPersons: t.TypeC<{
something: t.StringC;
persons: t.ArrayC<t.TypeC<{
name: t.IntersectionC<[t.TypeC<{
firstName: t.StringC;
lastName: t.StringC;
}>, t.PartialC<{
middleName: t.StringC;
}>]>;
cityName: t.StringC;
}>>;
}>;
And it gets worse when composing types.
Ah, so you are not satisfied by the type of the codec itself...
you are writing the schema twice though
// 1)
type User = {
userId: number
name: string
}
// 2)
const User: t.Type<User> = t.type({
userId: t.number,
name: t.string
})
What about this pattern?
const _User = t.type({
userId: t.number,
name: t.string
})
interface User extends t.TypeOf<typeof _User> {}
const User: t.Type<User> = _User
Works with brands too
const _UserId = t.brand(t.number, _ => true, 'UserId')
type UserId = t.TypeOf<typeof _UserId>
type UserIdO = t.OutputOf<typeof _UserId>
const UserId: t.Type<UserId, UserIdO> = _UserId
The described pattern solves the issue with verbose definitions and makes code less vulnerable to human factor issue I described above, which is much much better.
There are still few pros to using explicit types:
You get better IDE hints:

vs:

and field comments:

And you can also define decoders for _external_ types, I mean - there is still place for the way I'm using it.
Anyway, this is probably not the best place to discuss this issue :)
On the _refined_ topic - I don't think this should stop the progress with new brand combinator, I can add type alias locally (or even inline it as T & t.Brand<"Name">) until (_and if_) it will added to the library.
this is probably not the best place to discuss this issue
Indeed.. one last observation though: I used interface in my previous example because is less verbose but if you want to see the details you can use a type alias instead
const _User = t.intersection([
t.type({
userId: t.number,
/**
* Full user name
*/
name: t.string
}),
t.partial({
something: t.string,
anything: t.number
})
])
type User = t.TypeOf<typeof _User> // a type alias instead of an interface
const User: t.Type<User> = _User
On hover

Comments work already both with interfaces and type aliases

On the refined topic - I don't think this should stop the progress with new brand combinator, I can add type alias locally (or even inline it as T & t.Brand<"Name">) until (and if) it will added to the library.
:+1: thanks for your feedback