Hello there,
first of all, thanks for the great work you're doing in porting functional types to TS! I'm currently using fp-ts in one of the projects we're developing within the Italian Digital Transformation Team (Option and Either).
I'm now looking to integrate io-ts for validating the payload of API requests and generating error messages. The validation process is very clear to me, but I'm wondering if io-ts types (esp. refined types) can be used as compile time types.
For instance right now we have "tagged types" (an approach inspired by scalaz) like NonEmptyString that we can use within the code to make sure that only validated data gets handled (I believe you used a similar approach in newtype-ts).
Now, I'd like to use io-ts to validate the input payload and output a tagged type in some way.
Basically having something like toNonEmptyString that returns an Either<Errors, NonEmptyString>.
Here's what I tried so fa, without success. Basically Integer is a refinement of number, I'd like to be able to define a function that can only accept Integer types and only values that have been validates as correct Integers.
type Integer = t.TypeOf<typeof t.Integer>;
function f(i: Integer): void {
console.log(i);
}
const x = 1;
f(x); // type error
const y: Integer = x; // type error
if (t.Integer.is(x)) {
f(x); // no error
const z: Integer = x; // ok
}
thanks!
_update_ I accomplished what I was looking for by creating a new class an type, I wonder if I can accomplish the same goal with less code (e.g. using t.refinement)?
type Integer = number & {
readonly _tag: "Integer";
};
class IntegerType extends t.Type<any, Integer> {
public readonly _tag: "IntegerType" = "IntegerType";
constructor() {
super(
"Integer",
(v): v is Integer => typeof v === "number" && v % 1 === 0,
(s, c) => (this.is(s) ? t.success(s) : t.failure(s, c)),
t.identity
);
}
}
const Integer: IntegerType = new IntegerType();
function f(i: Integer): void {
console.log(i);
}
const x: number = 1;
f(x); // type error
const validation = Integer.validate(x, []);
validation.fold(console.log, f);
Ciao Federico,
basically you can tag any type
type Tagged<T, S, A> = t.Type<S, A & T>
export const tag = <T>() => <S, A>(type: t.Type<S, A>): Tagged<T, S, A> => type as any
interface INonEmptyStringTag {
readonly kind: 'INonEmptyStringTag'
}
const NonEmptyString = tag<INonEmptyStringTag>()(t.refinement(t.string, s => s.length > 0, 'NonEmptyString'))
type NonEmptyString = t.TypeOf<typeof NonEmptyString>
interface IWithinRangeStringTag<L extends number, H extends number> {
readonly lower: L
readonly higher: H
readonly kind: 'IWithinRangeStringTag'
}
const withinRangeString = <L extends number, H extends number, T extends IWithinRangeStringTag<L, H>>(
l: L,
h: H
): Tagged<T, any, string> =>
tag<T>()(t.refinement(t.string, s => s.length >= l && s.length < h, `WithinRangeStringTag(${l}, ${h})`))
const MyNumberArray = tag<{ kind: 'MyNumberArray' }>()(t.array(t.number))
Awesome, thanks! I'll give it a try!
Hi @gcanti ,
quick question, I'm having an issue with latest version of io-ts... could it be that the Tagged type needs to be changed?
I'm trying to create a type safe wrapper around fetch that takes an io-ts type as input and automatically validates the response of the request via decode. While working on this, I've encountered a compile error that comes up only when I use some tagged types, specifically when I use PatternString.
The code that doesn't compile is this, the code of the method is here and the error is:
ts/utils/api.ts(15,29): error TS2345: Argument of type '{ method: "get"; response_body_type: Type<string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">, string, mixed>; }' is not assignable to parameter of type 'IGetApiCall<string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">>'.
Types of property 'response_body_type' are incompatible.
Type 'Type<string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">, string, mixed>' is not assignable to type 'Type<string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">, string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">, mixed>'.
Types of property 'encode' are incompatible.
Type 'Encode<string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">, string>' is not assignable to type 'Encode<string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">, string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">>'.
Type 'string' is not assignable to type 'string & IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">'.
Type 'string' is not assignable to type 'IPatternStringTag<"^[A-Z]{6}[0-9LMNPQRSTUV]{2}[ABCDEHLMPRST][0-9LMNPQRSTUV]{2}[A-Z][0-9LMNPQRSTUV]{3}[A-Z]$">'.
The problem is a mismatch of the inferred Encode that (I think) is caused by the definition of our Tagged type here that has the type param O different from the type param A in t.Type.
I believe this was caused by the recent change of the type signature of t.Type and I think a possible solution could be to use the following signature for the Tagged type.
type Tagged<T, S extends t.mixed, A> = t.Type<A & T, A & T, S>;
what do you think?
Most helpful comment
Ciao Federico,
basically you can tag any type