Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: Type Builder API #9883

Open
weswigham opened this issue Jul 22, 2016 · 5 comments
Open

Proposal: Type Builder API #9883

weswigham opened this issue Jul 22, 2016 · 5 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@weswigham
Copy link
Member

weswigham commented Jul 22, 2016

Alongside the relationship API proposed in #9879, it would be very useful to expose an API to programatically build types - this is useful for making comparisons between a build-in type, and a type structure which isn't a part of the original compilation (lint rules, language service extensions, and external tools could find this a very useful API surface, and this is almost a prerequisite for a type provider-like feature). I have a protototype here, and the new API surface (excluding anything covered by #9879) looks like this:

interface TypeChecker {
  getTypeBuilder(): TypeBuilder;
}

interface TypeBuilder {
    startEnumType(): EnumTypeBuilder<Type>;
    startClassType(): ClassTypeBuilder<Type>;
    startInterfaceType(): InterfaceTypeBuilder<Type>;
    startTupleType(): TupleTypeBuilder<Type>;
    startUnionType(): UnionTypeBuilder<Type>;
    startIntersectionType(): IntersectionTypeBuilder<Type>;
    startAnonymousType(): AnonymousTypeBuilder<Type>;
    startNamespace(): NamespaceBuilder<Symbol>;
    startSignature(): SignatureBuilder<Signature>;

    // create an unbound type parameter, optionally with a constraint, for use in generic creation
    createTypeParameter(name: string, constraint?: Lazy<Type>): TypeParameter;
    // Useful for constructing reusable generics in your structures - probably need to make the params/type lazy
    createTypeAlias(name: string, params: TypeParameter[], type: Type): Symbol;
    // This way consumers can fill out generics - probably need to make the type/type arguments lazy
    getTypeReferenceFor(type: GenericType, ...typeArguments: Type[]): TypeReference;
    // Unsure about this one. Maybe there's a better way to make programmatic clodules and such? 
    mergeSymbols(symbolA: Symbol, symbolB: Symbol): void;
}

const enum BuilderMemberModifierFlags {
    None        = 0,
    Public      = 1 << 0,
    Protected   = 1 << 1,
    Private     = 1 << 2,
    Readonly    = 1 << 3
}

namespace TypeBuilderKind {
    export type Namespace = "Namespace";
    export const Namespace: Namespace = "Namespace";

    export type Signature = "Signature";
    export const Signature: Signature = "Signature";

    export type Anonymous = "Anonymous";
    export const Anonymous: Anonymous = "Anonymous";

    export type Interface = "Interface";
    export const Interface: Interface = "Interface";

    export type Class = "Class";
    export const Class: Class = "Class";

    export type Tuple = "Tuple";
    export const Tuple: Tuple = "Tuple";

    export type Union = "Union";
    export const Union: Union = "Union";

    export type Intersection = "Intersection";
    export const Intersection: Intersection = "Intersection";

    export type Enum = "Enum";
    export const Enum: Enum = "Enum";
}
type TypeBuilderKind = TypeBuilderKind.Namespace
    | TypeBuilderKind.Signature
    | TypeBuilderKind.Anonymous
    | TypeBuilderKind.Interface
    | TypeBuilderKind.Class
    | TypeBuilderKind.Tuple
    | TypeBuilderKind.Union
    | TypeBuilderKind.Intersection
    | TypeBuilderKind.Enum;

type Lazy<T> = T | (() => T);

interface BaseTypeBuilder<FinishReturnType> {
    finish(): FinishReturnType;
}

interface EnumTypeBuilder<FinishReturnType> extends BaseTypeBuilder<FinishReturnType> {
    addMember(name: string, value: number): this;
    addMember(name: string): this;
    isConst(flag: boolean): this;
    setName(name: string): this;
}

interface StructuredTypeBuilder<FinishReturnType> extends BaseTypeBuilder<FinishReturnType> {
    addMember(name: string, flags: BuilderMemberModifierFlags, type: Lazy<Type>): this;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Enum): EnumTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Namespace): NamespaceBuilder<this>;
    buildMember(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
}

interface ClassTypeBuilder<FinishReturnType> extends StructuredTypeBuilder<FinishReturnType>, TypeParameterBuilder {
    setName(name: string): this;

    setBaseType(type: Lazy<Type>): this;
    buildBaseType(kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildBaseType(kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildBaseType(kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildBaseType(kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildBaseType(kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;

    addImplementsType(type: Lazy<Type>): this;
    buildImplementsType(kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildImplementsType(kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildImplementsType(kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildImplementsType(kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildImplementsType(kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;

    addStatic(name: string, flags: BuilderMemberModifierFlags, type: Lazy<Type>): this;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
    // buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Enum): EnumTypeBuilder<this>;
    // buildStatic(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Namespace): NamespaceBuilder<this>;

    addConstructSignature(sig: Lazy<Signature>): this;
    buildConstructSignature(): SignatureBuilder<this>;
}

interface ObjectTypeBuilder<FinishReturnType> extends BaseTypeBuilder<FinishReturnType> {
    addCallSignature(sig: Lazy<Signature>): this;
    buildCallSignature(): SignatureBuilder<this>;

    addConstructSignature(sig: Lazy<Signature>): this;
    buildConstructSignature(): SignatureBuilder<this>;

    addStringIndexType(name: string, flags: BuilderMemberModifierFlags, type: Lazy<Type>): this;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildStringIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;

    addNumberIndexType(name: string, flags: BuilderMemberModifierFlags, type: Lazy<Type>): this;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildNumberIndexType(name: string, flags: BuilderMemberModifierFlags, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
}

interface TypeParameterBuilder {
    // This overload is useful for making a TypeParameter yourself and threading it back in (as a type)
    // elsewhere in order to flow generics through a generated type
    addTypeParameter(type: Lazy<TypeParameter>): this;
    addTypeParameter(name: string, constraint?: Lazy<Type>): this;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildTypeParameter(name: string, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
}

interface InterfaceTypeBuilder<FinishReturnType> extends ObjectTypeBuilder<FinishReturnType>, TypeParameterBuilder, StructuredTypeBuilder<FinishReturnType> {
    setName(name: string): this;

    addBaseType(type: Lazy<Type>): this;
    buildBaseType(kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildBaseType(kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
}

interface CollectionTypeBuilder<FinishReturnType> extends BaseTypeBuilder<FinishReturnType> {
    addType(type: Lazy<Type>): this;
    // buildMemberType(kind: TypeBuilderKind.Namespace): NamespaceBuilder<this>;
    // buildMemberType(kind: TypeBuilderKind.Enum): EnumTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildMemberType(kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
}

interface TupleTypeBuilder<FinishReturnType> extends CollectionTypeBuilder<FinishReturnType> {}
export interface UnionTypeBuilder<FinishReturnType> extends CollectionTypeBuilder<FinishReturnType> {}
export interface IntersectionTypeBuilder<FinishReturnType> extends CollectionTypeBuilder<FinishReturnType> {}
export interface AnonymousTypeBuilder<FinishReturnType> extends ObjectTypeBuilder<FinishReturnType> {
    addMember(name: string, type: Lazy<Type>): this;
    buildMember(name: string, kind: TypeBuilderKind.Enum): EnumTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Namespace): NamespaceBuilder<this>;
    buildMember(name: string, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
}

interface NamespaceBuilder<FinishReturnType> extends BaseTypeBuilder<FinishReturnType> {
    setName(name: string): this;

    addExport(symbol: Lazy<Symbol>): this;
    addExport(name: string, type: Lazy<Type>): this;
    buildExport(name: string, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Enum): EnumTypeBuilder<this>;
    buildExport(name: string, kind: TypeBuilderKind.Namespace): NamespaceBuilder<this>;
}
interface SignatureBuilder<FinishReturnType> extends BaseTypeBuilder<FinishReturnType>, TypeParameterBuilder {
    setName(name: string): this;
    setConstructor(flag: boolean): this;

    addParameter(name: string, type: Lazy<Type>): this;
    buildParameter(name: string, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildParameter(name: string, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildParameter(name: string, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildParameter(name: string, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildParameter(name: string, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildParameter(name: string, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildParameter(name: string, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;

    setRestParameter(name: string, type: Lazy<Type>): this;
    buildRestParameter(name: string, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildRestParameter(name: string, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildRestParameter(name: string, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildRestParameter(name: string, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildRestParameter(name: string, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildRestParameter(name: string, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildRestParameter(name: string, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;

    setReturnType(type: Lazy<Type>): this;
    buildReturnType(kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildReturnType(kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildReturnType(kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildReturnType(kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildReturnType(kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildReturnType(kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildReturnType(kind: TypeBuilderKind.Signature): SignatureBuilder<this>;

    setThisType(type: Lazy<Type>): this;
    buildThisType(kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildThisType(kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildThisType(kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildThisType(kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildThisType(kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildThisType(kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildThisType(kind: TypeBuilderKind.Signature): SignatureBuilder<this>;

    setPredicateType(argument: string, constraint: Lazy<Type>): this;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Class): ClassTypeBuilder<this>;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Interface): InterfaceTypeBuilder<this>;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Tuple): TupleTypeBuilder<this>;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Union): UnionTypeBuilder<this>;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Intersection): IntersectionTypeBuilder<this>;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Anonymous): AnonymousTypeBuilder<this>;
    buildPredicateType(argument: string, kind: TypeBuilderKind.Signature): SignatureBuilder<this>;
}

which, in the end, looks like this when used:

const c1 = builder.startClassType()
    .addMember("x", BuilderMemberModifierFlags.Readonly, checker.getNumberType())
    .addMember("y", BuilderMemberModifierFlags.Readonly, checker.getNumberType())
    .buildConstructSignature()
        .addParameter("x", checker.getNumberType())
        .addParameter("y", checker.getNumberType())
        .setReturnType(getc1)
        .finish()
    .buildImplementsType(TypeBuilderKind.Interface)
        .addMember("x", BuilderMemberModifierFlags.Readonly, checker.getAnyType())
        .addMember("y", BuilderMemberModifierFlags.Readonly, checker.getAnyType())
        .setName("PointLike")
        .finish()
    .buildStatic("from", BuilderMemberModifierFlags.Public, TypeBuilderKind.Signature)
        .buildParameter("point", TypeBuilderKind.Anonymous)
            .addMember("x", checker.getNumberType())
            .addMember("y", checker.getNumberType())
            .finish()
        .setReturnType(() => c1)
        .finish()
    .finish();

if (checker.isAssignableTo(c1, someOtherType)) {
  // ...
}

The goal is to have a fluent API capable of creating an immutable version of any type or structure expressible within the type system. From my prototyping experience, there are only a handful of locations where a link or cache needed to be added to ensure the checker never needs to try to access an AST backing the types - so these "synthetic" or "declarationless" types actually tend to work fairly well within the checker once those are in-place. A few more links or hooks likely need to be added on top of those to ensure that the values are only retrieved lazily (rather than applied eagerly on API type creation). The laziness is actually super important for creating circularly referential types with an immutable API (otherwise a type can't really get a handle to it's finished self before it is done).

Known shortcoming: There's no method (in the type definition I have posted here) for making a member mapped to a well-known symbol, such as Symbol.iterator. I think this would just surface as parameters for object/class/interface members names taking string | WellKnownSymbol and having a method for looking up well-known symbols, though.

cc: @ahejlsberg @mhegazy @DanielRosenwasser @rbuckton

I'd love to hear everyone's thoughts on this - the general fluent creation API I've been working off and on for the last few weeks, and I think it has real promise for letting consumers make valid types safely (and the intellisense support is top notch).

@yortus
Copy link
Contributor

yortus commented Jul 22, 2016

Just curious, could the API also provide a way to 'eval up' a type for simple cases, for brevity sake, e.g.:

const c1 = builder.eval(`class { readonly x: number; readonly y: number; }`);
if (checker.isAssignableTo(c1, someOtherType)) {
  // ...
}

@HerringtonDarkholme
Copy link
Contributor

@yortus A eval like API is more approachable for new-comers. But for TypeScript compiler a low-level builder is more desirable. A low-level builder is more powerful for building complex types, e.g. conditional fields where arbitrary logic needs to be executed. And low-level builder does not mix parsing complexity in.

eval like API can be built upon builder API, as a library, like babel-template

@mohsen1
Copy link
Contributor

mohsen1 commented May 1, 2017

That's why I couldn't find ts.createTypeAliasDeclaration then!

@RyanCavanaugh RyanCavanaugh added the Needs Investigation This issue needs a team member to investigate its status. label May 24, 2017
@DanielRosenwasser DanielRosenwasser added Suggestion An idea for TypeScript and removed Needs Investigation This issue needs a team member to investigate its status. labels Aug 29, 2017
@RyanCavanaugh RyanCavanaugh added the Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature label Aug 15, 2018
@mheiber
Copy link
Contributor

mheiber commented Oct 3, 2019

Is this proposal dead?
I think I heard that a downside of this kind of approach is that it could be hard for devs consuming the types to debug.

@unional
Copy link
Contributor

unional commented Oct 10, 2020

I'm releasing a simple version of this for checking types in runtime in type-plus: unional/type-plus#71

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants