Hello @gcanti!
First of all, let me say thank you! You are doing a great great job and making typescript world much better!
Is that possible to make some kind of type aliases for inferred types?
I'm using io-ts to create a library to validate Swagger schema and generate some code based on. But working with complex (especially recursive) types makes hard to keep track of what type we working on now, i.e. what type is that variable etc. Let's take an example:
const TypeA = io.type({
$ref: io.string,
});
const TypeB = io.type({
foo: io.string,
typeA: TypeA,
});
const TypeC = io.type({
foo: io.string,
typeB: TypeB,
});
type Type = io.TypeOf<typeof TypeC>;
type Type will be inferred as:
type Type = {
foo: string;
typeB: io.TypeOfProps<{
foo: io.StringType;
typeA: io.InterfaceType<{
$ref: io.StringType;
}, io.TypeOfProps<{
$ref: io.StringType;
}>, io.OutputOfProps<{
$ref: io.StringType;
}>, io.mixed>;
}>;
}
And that is cool, but when we have complex types it is almost not possible to understand what type we have now.
It would be nice to be possible to make some kind of type aliases, to see something like:
type Type2 = {
foo: string;
typeB: TypeB;
}
The only way I found is to make intermediate interface and manually describe the type. For example:
interface TTypeA {
$ref: string;
}
const propsA = {
$ref: io.string,
};
const TypeA: io.InterfaceType<typeof propsA, TTypeA, io.OutputOfProps<typeof propsA>, io.mixed> = io.type(propsA);
const TypeB = io.type({
foo: io.string,
typeA: TypeA,
});
type Type = io.TypeOf<typeof TypeB>;
In that case Type is:
type Type = {
foo: string;
typeA: TTypeA;
}
Generally, that approach works, but looks a bit ugly. Do you have any ideas on that?
That s the way to achieve it, also you may give the same name to the interface and io-ts codec in order to be able to import both at the same time.
export interface MyTypeA {
$ref: string;
}
export const MyTypeA: io.Type<MyTypeA> = io.type({
$ref: io.string,
});
// other file:
import { MyTypeA } from '...' // imports both the Type (type level) and the Codec (value level)
As stated here https://github.com/gcanti/io-ts-codegen/issues/15#issuecomment-384353222 erasing the actual "kind" of the runtime type in favour of a generic Type<A> can be a problem
interface A {
type: 'A'
foo: string
}
interface B {
type: 'B'
bar: number
}
// A now is simply a Type<A>
const A: t.Type<A> = t.type({
type: t.literal('A'),
foo: t.string
})
// B is an InterfaceType<..., ..., ..., mixed>
const B = t.type({
type: t.literal('B'),
bar: t.number
})
const U = t.taggedUnion('type', [A, B]) // static error since A is no more a Tagged<'type'>
We should find a way to alias a runtime type while preserving its "kind".
What about
// other overloadings here...
function alias<P extends t.Props, A, O, I>(
type: t.InterfaceType<P, A, O, I>
): <AA extends A, OO extends O = O, II extends I = I>() => t.InterfaceType<P, AA, OO, II>
function alias<A, O, I>(
type: t.Type<A, O, I>
): <AA extends A, OO extends O = O, II extends I = I>() => t.Type<AA, OO, II>
function alias<A, O, I>(
type: t.Type<A, O, I>
): <AA extends A, OO extends O = O, II extends I = I>() => t.Type<AA, OO, II> {
return () => type as any
}
interface A {
type: 'A'
foo: string
}
interface B {
type: 'B'
bar: number
}
const A = alias(
t.type({
type: t.literal('A'),
foo: t.string
})
)<A>()
const B = alias(
t.type({
type: t.literal('B'),
bar: t.number
})
)<B>()
export const U = t.taggedUnion('type', [A, B]) // ok
/cc @leemhenson
We're having the same issue and using specific 'wrappers' to narrow the runtime types.
Your overloading approach @gcanti really nails it!
@gcanti that's great but not extensible for custom types. (have not found a way to shoehorn it with augmentations).
Note however that the signature could be represented via that scheme (which can be extended):
type Extensible = (<P extends t.Props, A, O, I>(
type: t.InterfaceType<P, A, O, I>
) => <AA extends A, OO extends O = O, II extends I = I>() => t.InterfaceType<P, AA, OO, II>)
& (<A, O, I>(
type: t.Type<A, O, I>
) => <AA extends A, OO extends O = O, II extends I = I>() => t.Type < AA, OO, II >)
or using derivation of interfaces and additional overloads (I guess, not verified).
but not extensible for custom types, have not found a way to shoehorn it with augmentations
@sledorze weird, what's the problem?
Note: alias is unsafe with respect to additional fields declared in the type alias
interface A {
type: 'A'
foo: string
a: boolean // additional field
}
const A = alias(
t.type({
type: t.literal('A'),
foo: t.string
})
)<A>() // no error!
Here's a safer version (requires TypeScript 2.8.x though)
export type Exact<T, X extends T> = T & { [K in Exclude<keyof X, keyof T>]?: never }
export function alias<P extends t.Props, A, O, I>(
type: t.InterfaceType<P, A, O, I>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O, II extends Exact<I, II> = I>() => t.InterfaceType<P, AA, OO, II>
export function alias<A, O, I>(
type: t.Type<A, O, I>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O, II extends Exact<I, II> = I>() => t.Type<AA, OO, II>
export function alias<A, O, I>(
type: t.Type<A, O, I>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O, II extends Exact<I, II> = I>() => t.Type<AA, OO, II> {
return () => type as any
}
interface A {
type: 'A'
foo: string
a: boolean // additional field
}
const A = alias(
t.type({
type: t.literal('A'),
foo: t.string
})
)<A>()
/*
static error:
Type 'A' does not satisfy the constraint 'Exact<TypeOfProps<{ type: LiteralType<"A">; foo: StringType; }>, A>'.
Type 'A' is not assignable to type '{ type?: "A" | undefined; foo?: string | undefined; a?: undefined; }'.
Types of property 'a' are incompatible.
Type 'boolean' is not assignable to type 'undefined'.
*/
EDIT: Here's a version of Exact compatible with TypeScript 2.7.x
export type Exact<T, X extends T> = T &
{ [K in ({ [K in keyof X]: K } & { [K in keyof T]: never } & { [key: string]: never })[keyof X]]?: never }
@gcanti tried to use augmentations to encode extensibles Type alias like this:
export interface Foo<T extends t.ArrayType<any, any, any, any>, X extends t.TypeOf<T>> {
ArrayType: t.ArrayType<T['type'], X, t.OutputOf<T>, t.InputOf<T>>
}
export interface Foo<T extends t.InterfaceType<any, any, any, any>, X extends t.TypeOf<T>> {
InterfaceType: t.InterfaceType<T['props'], X, t.OutputOf<T>, t.InputOf<T>>
}
type SupportedTypes = Foo<any, any>[keyof Foo<any, any>]
const aliasType = <T extends SupportedTypes>(x: T) => <R extends t.TypeOf<T>>() : Foo<T, R>[typeof x._tag] => 1 as any
but it errors on:
All declarations of 'Foo' must have identical type parameters.
Here's a recap of the findings in io-ts-codegen
2 possible helper functions
// drops the runtime type "kind"
export function clean<A, O = A>(type: t.Type<A, O>): t.Type<A, O> {
return type as any
}
export type Exact<T, X extends T> = T & { [K in Exclude<keyof X, keyof T>]?: never }
// keeps the runtime type "kind"
export function alias<P extends t.Props, A, O>(
type: t.PartialType<P, A, O>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O>() => t.PartialType<P, AA, OO>
export function alias<P extends t.Props, A, O>(
type: t.StrictType<P, A, O>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O>() => t.StrictType<P, AA, OO>
export function alias<P extends t.Props, A, O>(
type: t.InterfaceType<P, A, O>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O>() => t.InterfaceType<P, AA, OO>
export function alias<A, O>(
type: t.Type<A, O>
): <AA extends Exact<A, AA>, OO extends Exact<O, OO> = O>() => t.Type<AA, OO> {
return type as any
}
The Pattern
// private runtime type
const _Foo = t.type({
foo: t.string
})
// type alias
export interface Foo extends t.TypeOf<typeof _Foo> {}
// output alias
export interface FooO extends t.OutputOf<typeof _Foo> {}
Two possible options for the exported runtime type
// a clean _Foo which drops the kind...
export const Foo = clean<Foo, FooO>(_Foo)
/*
Foo: t.Type<Foo, FooO, t.mixed>
*/
// ... or an alias of _Foo which keeps the kind
export const Foo = alias(_Foo)<Foo, FooO>()
/*
Foo: t.InterfaceType<{
foo: t.StringType;
}, Foo, FooO, t.mixed>
*/
@gcanti Ok, so far we've used some homegrown versions of the clean function.
Lately we wanted to have more compile time (rather than runtime) type checking for conversions (swagger, testCheck generators) and are in need to preserve the kinds.
However, we're also using a lot of custom types (jsJoda types, currency types, etc..) and that implies the solution used for the alias approach to be extensible.
I'm currently thinking about dropping compile time type checking because keeping the kind structure make the types really too verbose (compared to clean) and make errors involving them unreadable.
that implies the solution used for the alias approach to be extensible
Can't you just add more overloadings?
@gcanti I can add more overloading but AFAIK these must belong to the same file, which is the extensibility issue.
@sledorze using module augmentation should do the trick
Example
library definition file node_modules/foo/index.d.ts
export declare function foo(x: 'a'): 'a'
app code
import { foo } from 'foo'
declare module 'foo' {
export function foo(x: 'b'): 'b'
}
export const x1 = foo('a') // ok because of original definition file
export const x2 = foo('b') // now ok because of module augmentation
Ok, I'm going to add clean / alias to the library and the described pattern to the README file
@gcanti will you use it in io-ts-codegen?
Possibly with a CustomTypeDeclaration
import * as t from 'io-ts-codegen'
const alias = (name: string, type: t.TypeReference): t.CustomTypeDeclaration =>
t.customTypeDeclaration(
name,
`export interface ${name} extends t.TypeOf<typeof _${name}> {}
export interface ${name}Output extends t.OutputOf<typeof _${name}> {}
export interface ${name}Props extends t.PropsOf<typeof _${name}> {}`,
`const _${name} = ${t.printRuntime(type)}
export const ${name} = t.alias(_${name})<${name}, ${name}Output, ${name}Props>()`,
t.getNodeDependencies(type)
)
const person = alias('Person', t.interfaceCombinator([t.property('name', t.stringType)]))
console.log(t.printStatic(person))
console.log(t.printRuntime(person))
/*
export interface Person extends t.TypeOf<typeof _Person> {}
export interface PersonOutput extends t.OutputOf<typeof _Person> {}
export interface PersonProps extends t.PropsOf<typeof _Person> {}
const _Person = t.interface({
name: t.string
})
export const Person = t.alias(_Person)<Person, PersonOutput, PersonProps>()
*/
@gcanti indeed! thanks!
Most helpful comment
@gcanti will you use it in io-ts-codegen?