Herein lies a question about statically inferring the signature of runtime types, as seen in popular libraries like zod and io-ts.
If you want to see an example in action, check out this TS playground link.
Let's say we're attempting to model type information for runtime usage. To start off, we can define the following enum called Type
:
enum Type {
Boolean = "Boolean",
Int = "Int",
List = "List",
Union = "Union",
}
This runtime type system should be able to handle booleans, integers, unions, and lists.
The base type structure looks like this:
interface Codec<T extends Type> {
type: T;
}
For boolean and integer types, they utilize this base type in the following manner:
Boolean:
class BooleanCodec implements Codec<Type.Boolean> {
type = Type.Boolean as const;
}
Integer:
class IntCodec implements Codec<Type.Int> {
type = Type.Int as const;
}
The union type takes an array of types to combine them:
class UnionCodec<C extends Codec<Type>> implements Codec<Type.Union> {
type = Type.Union as const;
constructor(public of: C[]) {}
}
And the list type defines the type of which its elements are made up:
class ListCodec<C extends Codec<Type>> implements Codec<Type.List> {
type = Type.List as const;
constructor(public of: C) {}
}
Now let's create a list consisting of booleans or integers:
const listOfBooleanOrIntCodec = new ListCodec(
new UnionCodec([
new BooleanCodec(),
new IntCodec(),
]),
);
This results in the object below:
{
type: Type.List,
of: {
type: Type.Union,
of: [
{
type: Type.Boolean,
},
{
type: Type.Int,
},
]
}
}
Such codec would have a signature of
ListCodec<UnionCodec<BooleanCodec | IntCodec>>
.
Sometimes there might be cycles within a given codec, making mapping the type signature more complex. How do we go from the above representation to (boolean | number)[]
? Also, does it incorporate deep nesting of codecs?
Decoding BooleanCodec
or IntCodec
is relatively straightforward... However, decoding UnionCodec
and ListCodec
requires recursive operation. I attempted the following:
type Decode<C extends Codec<Type>> =
// if it's a list
C extends ListCodec<Codec<Type>>
? // and we can infer what it's a list of
C extends ListCodec<infer O>
? // and the elements are of type codec
O extends Codec<Type>
? // recurse to get an array of the element(s') type
Decode<O>[]
: never
: never
: // if it's a union
C extends UnionCodec<Codec<Type>>
// and we can infer what it's a union of
? C extends UnionCodec<infer U>
// and it's a union of codecs
? U extends Codec<Type>
// recurse to return that type (which will be inferred as the union)
? Decode<U>
: never
: never
// if it's a boolean codec
: C extends BooleanCodec
// return the boolean type
? boolean
// if it's ant integer codec
: C extends IntCodec
// return the number type
? number
: never;
Regrettably, it throws errors like
Type alias 'Decode' circularly references itself
and Type 'Decode' is not generic
.
I am curious if achieving this kind of cyclical type-mapping is possible and how to make a utility like Decode
function effectively. Any assistance on this matter would be highly appreciated. Thank you!