question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Safe decorator implementation with ^ type operator.

See original GitHub issue

Outline

Nowadays, lots of famous JavaScript libraries like typeorm or nestjs are supporting the decorator feature. However, the decorator feature is not suitable for core philosophy of the TypeScript; the safe implementation.

When defining a decorator onto a variable, duplicated definition in the variable type must be written. It’s very annoying and even dangerous when different type be defined in the variable type. The TypeScript compiler can’t detect the mis-typed definition.

For an example, look at the below code, then you may find something weird. Right, column types of title and content are varchar and text. However, their instance types are number and boolean. Their instance type must be string, but there would not be any compile error when using the TypeScript. It’s the dangerous characteristic of the decorator what I want to say.

@Table()
export class BbsArticle extends Model<BbsArticle>
{
    @Column("varchar", {
        restrict: ["NORMAL", "NOTICE", "QUESTION", "SUGGESTION"],
        default: "NORMAL"
    })
    public category!: "NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION";

    @Column("varchar")
    public title!: number;

    @Column("varchar", { nullable: true })
    public sub_title!: string | null;

    @Column("text")
    public content!: boolean;

    @Column("int", { unsigned: true, default: 1 })
    public hits!: number;
}

function Column<Type extends TypeList, Options extends Options<Type>>
    (type: string, options?: Options<Type>): SomeFunction;

I think such dangerous characteristic is the reason why TypeScript is supporting the decorator as experimental feature for a long time. I want to suggest an alternative solution that can make decorator to be much safer, so that TypeScipt can adapt the decorator as a standard feature.

Key of the alternative solution is to defining the decorator feature not in front of the variable, but in the variable type definition part. To implement the variable type defined decarator, I think a new pseudo type operator ^ is required.

@Table()
export class BbsArticle extends Model<BbsArticle>
{
    // ("NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION") ^ SomeFunction
    public category!: @Column("varchar", {
        restrict: ["NORMAL", "NOTICE", "QUESTION", "SUGGESTION"],
        default: "NORMAL"
    });

    // string ^ SomeFunction
    public title!: @Column("varchar");

    // (string | null) ^ SomeFunction
    public sub_title!: @Column("varchar", { nullable: true });

    // string ^ SomeFunction
    public content!: @Column("text");

    // number ^ SomeFunction
    public hits!: @Column("int", { 
        unsigned: true, 
        default: 1 
    });
}

function Column<Type extends TypeList, Options extends Options<Type>>
    (type: Type, options?: Options): DeductType<Type, Options> ^ SomeFunction;

Pseudo type operator ^

// Decorator is a function returning a function.
function SomeDecorator(): Function;

// Variable type cannot be expressed
var someVariable: @SomeDecorator();

In JavaScript, decorator is a type of function returning a meta function. In the ordinary TypeScript, the decorator function would be represented like upper code. Therefore, there’s no way to express the variable type who’re using the decorator.

Therefore, I suggest a new pseudo type operator, ^ symbol. With the ^ symbol, expressing both variable and decorator types, at the same time, are possible. Left side of the ^ operator would be the variable type and that can be assigned or be read as a value. The right side would be a pseudo type representing return type of the target decorator.

  • Left ^ Right
    • Left: Variable type to be assigned or to be read.
    • Right: Pseudo type for meta implementation.

Within framework of the type meta programming, both left and right side of the ^ symbol can be all used. Extending types from both left and right side are all possible. However, assigning and reading variable’s value, it’s only permitted to the left side’s type.

function SomeDecorator(): number ^ Function
{
    return function ()
    {
        // implementation code
    };
}
type SomeType = ReturnType<SomeDecorator>;

// TYPE EXTENSIONS ARE ALL POSSIBLE
type ExtendsNumber = SomeType extends number ? true : false; // true
type ExtendsColumn = SomeType extends Function ? true : false; // true

// ASSIGNING VALUE IS POSSIBLE
let x: SomeType = 3; // no error

// ASSIGNING THE DECORATOR FUNCTION IS NOT POSSIBLE
let decorator: Function = () => {};
x = decorator; // be compile error

Appendix

ORM Components

If the safe decorator implementation with the new ^ symbol is realized, there would be revolutionary change in ORM components. TypeScript would be the best programming language who can represents database table exactly through the ORM component and the safe decorator implementation.

It would be possible to represent the exact columns only with decorators. Duplicated definitions on the member variable types, it’s not required any more. Just read the below example ORM code, and feel which revolution would come:

@Table()
export class BbsArticle
    extends Model<BbsArticle>
{
    /* -----------------------------------------------------------
        COLUMNS
    ----------------------------------------------------------- */
    // number ^ IncrementalColumn<"int">
    public readonly id!: @IncrementalColumn("int");

    // number ^ ForeignColumn<ForeignColumn>
    public bbs_group_id!: @ForeignColumn(() => BbsGroup);

    // (number | null) ^ ForeignColumn<BbsArticle, Options>
    public parent_article_id!: @ForeignColumn(() => BbsArticle, { 
        index: true, 
        nullable: true 
    });

    // ("NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION") ^ Column<"int", Options>
    public category!: @Column("varchar", {
        restrict: ["NORMAL", "NOTICE", "QUESTION", "SUGGESTION"],
        default: "NORMAL"
    });

    // string ^ Column<"varchar", Options>
    public writer!: @Column("varchar", {
        index: true, 
        default: () => Random.characters(16) 
    });

    // string ^ Column<"varchar">
    public password!: @Column("varchar");

    // string ^ Column<"varchar">
    public title!: @Column("varchar");

    // (string | null) & Column<"varchar", Options>
    public sub_title!: @Column("varchar", {
        nullable: true 
    }); 

    // string ^ Column<"text">
    public content!: @Column("text");

    // number ^ Column<"int", Options>
    public hits!: @Column("int", {
        unsigned: true,
        default: 0
    });

    // Date ^ CreationTimeColumn
    public created_at!: @CreationTimeColumn();

    // (Date | null) ^ UpdationTimeColumn
    public updated_at!: @UpdationTimeColumn();

    // (Date | null) ^ SoftDeletionColumn
    public deleted_at!: @SoftDeletionColumn();

    /* -----------------------------------------------------------
        RELATIONSHIPS
    ----------------------------------------------------------- */
    public getGroup(): Promise<BbsGroup>
    {
        return this.belongsTo(BbsGroup, "bbs_group_id");
    }

    public getParent(): Promise<BbsArticle | null>
    {
        return this.belongsto(BbsArticle, "parent_article_id");
    }

    public getChildren(): Promise<BbsArticle[]>
    {
        return this.hasMany(BbsArticle, "parent_article_id");
    }

    public getTags(): Promise<BbsTag[]>
    {
        return this.hasManyToMany(BbsTag, BbsArticleTag, "bbs_tag_id", "bbs_article_id");
    }
}

If this issue be adopted, so that the safe decorator implementation with the ^ symbol is realized in the future TypeScript compiler, even join relationship can be much safer.

Because foreign columns are defined with the safe decorator, member variables of the columns have exact information about the reference. Therefore, defining join relationship can be safe with type meta programming like below:

export abstract class Model<Entity extends Model<Entity>>
{
    protected async belongsTo<
            Target extends Model<Target>, 
            Field extends SpecialFields<Entity, ForeignColumn<Target>>>
        (target: CreatorType<Target>, field: Field): 
            Promise<Entity[Field] extends ForeignColumn<Target, { nullable: true }>
                ? Target | null
                : Target>;

    protected async hasOne<
            Target extends Model<Target>,
            Field extends SpecialFields<Target, ForeignColumn<Entity>>,
            Symmetric extends boolean>
        (
            target: ObjectType<Target>, 
            field: Field, 
            symmetric: Symmetric
        ): Promise<Symmetric extends true ? Target : Target | null>;

    protected async hasMany<
            Target extends Model<Target>,
            Field extends SpecialFields<Target, ForeignColumn<Entity>>>
        (target: ObjectType<Target>, field: Field): Promise<Target[]>;

    // 1: M: N => 1: X
    protected hasManyToMany<
            Target extends Model<Target>,
            Route extends Model<Route>,
            TargetField extends SpecialFields<Route, ForeignColumn<Target>>,
            MyField extends SpecialFields<Route, ForeignColumn<Entity>>>
        (
            target: ObjectType<Target>,
            route: ObjectType<Route>,
            targetField: TargetField,
            myField: MyField
        ): Promise<Target[]>;

    // M: N => 1: M: 1
    protected hasManyThrough<
            Target extends Model<Target>,
            Route extends Model<Route>,
            TargetField extends SpecialFields<Target, ForeignColumn<Route>>,
            RouteField extends SpecialFields<Route, ForeignColumn<Entity>>>
        (
            target: ObjectType<Target>,
            route: ObjectType<Route>,
            targetField: TargetField,
            routeField: RouteField
        ): Promise<Target[]>;
}

Also, the safe decorator can make intializer construction much safer, too.

In the case of typeorm, using strict type checking options are discouraged. It’s because the old decorator can’t express the target variable’s detailed options like nullable or auto-assigned default value. Therefore, in the case of typeorm, initializer constructor is not supported. Even massive insertion methods are using the dangerous Partial type, because it can’t distinguish which field is whether essential or optional.

export module "typeorm"
{
    export class BaseEntity<Entity extends BaseEntity<Entity>>
    {
        // NO INITIALIZER CONSTRUCTOR EXISTS
        public constructor();

        // RECORDS ARE DANGEROUS (PARTIAL) TYPE
        // AS CANNOT DISTINGUISH WHETHER ESSENTIAL OR OPTINAL
        public static insert<Entity extends BaseEntity<Entity>>
            (
                this: CreatorType<Entity>, 
                records: Partial<Entity>[] 
            ): Promise<Entity[]>;
    }
}

However, if safe decorator implementation with ^ type operator is realized, supporting intializer constructor and massive insertion method with exact type are possible. As decorator defining each column contains the exact type information, distinguishing whether which field is essential or optional.

export class Model<Entity extends Model<Entity>>
{
    public static insert<Entity extends Model<Entity>>
        (this: CreatorType<Entity>, records: Model.Props<Entity>[]): Promise<Entity[]>;

    /**
     * Initializer Constructor
     * 
     * @param props Properties would be assigned to each columns
     */
    public constructor(props: Model.IProps<Entity>);
}

export namespace Model
{
    export type Props<Entity extends Model<Entity>>
        = OmitNever<RequiredProps<Entity, true>>
        & Partial<OmitNever<RequiredProps<Entity, false>>>;
    
    type RequiredProps<Entity extends Model<Entity>, Flag extends boolean> = 
    {
        [P in keyof Entity]: Entity[P] extends ColumnBase<infer Name, infer Options>
            ? IsRequired<Entity[P], Name, Options> extends Flag
                ? ColumnBase.Type<Name, Options>
                : never
            : never
    };

    type IsRequired<
            Column extends ColumnBase<Name, Options>, 
            Name extends keyof ColumnBase.TypeList, Options> =
        Column extends IncrementalColumn<any> ? false
        : Column extends UuidColumn<any> ? false
        : Options extends INullable<true> ? false
        : Options extends IDefault<any> ? false
        : true;
}

type Model.Props<BbsArticle> = 
{
    id?: number;
    bbs_group_id: number;
    parent_article_id?: number | null;
    category?: "NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION";

    writer: string;
    password: string;

    title: string;
    sub_title?: string | null;
    content: string;
    hits?: number;

    created_at?: Date;
    updated_at?: Date | null;
    deleted_at?: Date | null;
};

API Controllers

In nowadays, many JavaScript libraries like nestjs are wrapping express framework with decorator for convenient. However, wrapping features of express components with decorator, it loses chance to detecting m is-type-usage error in the compile level.

However, if safe decorator implementation with the ^ symbol is used, it also can be used safely. I’ll not write the detailed description about the below code. Just look and feel what the safe decorator means:

@Controller("bbs/:group/articles")
export class BbsArticlesController
{
    @Get()
    public index
        (
            httpReq: @HttpRequest(),
            group: @Param("group", "string"),
            input: @Query<IPage.IRequest>()
        ): Promise<IPage<IArticle.ISummary>[]>;

    @Get(":id")
    public async at
        (
            httpReq: @HttpRequest(),
            group: @Param("group", "string"),
            id: @Param("id", "number")
        ): Promise<IArticle>;

    @Post()
    public async store
        (
            httpReq: @HttpRequest(), 
            group: @Param("group", "string"),
            input: @RequestBody<IArticle>()
        ): Promise<IArticle>;

    @Put(":id")
    public async update
        (
            httpReq: @HttpRequest(),
            group: @Param("group", "string"),
            id: @Param("id", "number"),
            input: @RequestBody<Partial<IArticle>>()
        ): Promise<IArticle>;
}

Issue Analytics

  • State:open
  • Created 3 years ago
  • Reactions:15
  • Comments:8 (1 by maintainers)

github_iconTop GitHub Comments

1reaction
fatcerberuscommented, May 18, 2020

It seems like what you want is actually not decorators (which are a runtime feature) but in fact some kind of branded type?

1reaction
samchoncommented, May 18, 2020

@fatcerberus

I’d thought that there wouldn’t be any problem defining decorator on the variable definition (let variable: @decorator), because type definitions are a type of virtual code that would not be compiled out to the JavaScript source code. However, listening you opinion, it can be a violation of the TypeScript’s own strategy.

If defining decorator on the variable definition ((let variable: @decorator)) is violating the TypeScript’s own strategy, I want to suggest another way. It’s using the implicit type definition. When using a decorator function who is using the ^ type operator, the target variable’s type would be defined implicitly like below:

@Table()
export class BbsArticle extends Model<BbsArticle>
{
    @Column("varchar", {
        restrict: ["NORMAL", "NOTICE", "QUESTION", "SUGGESTION"],
        default: "NORMAL"
    })
    public category!: ("NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION") ^ SomeFunction;
        // exact explicit type

    @Column("varchar")
    public title!; // implicit type -> be (string & SomeFunction)

    @Column("varchar", { nullable: true })
    public sub_title: string; // origin type is (string ^ SomeFunction), but no problem

    @Column("text")
    public content!: boolean; // mis-explicit type -> throws compile error

    @Column("int", { unsigned: true, default: 1 })
    public hits!; // implicit type -> (be number ^ SomeFunction)
}

function Column<Type extends TypeList, Options extends Options<Type>>
    (type: string, options?: Options<Type>): SomeFunction;

In similar reason, using ^ (XOR) symbol seems not validate, it’s not a matter to changing the ^ symbol to another one. My goal is just to designing a safe decorator who can let TypeScript to move decorator feature from experimental to standard.

If you’ve an idea about the safe decorator, let’s argue about it.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Safe decorator implementation with "^" type operator. : r/typescript
Therefore, I suggested an alternative solution for the decoator, who can check type validation in the compile level. I named it as the...
Read more >
Documentation - Decorators - TypeScript
A Decorator is a special kind of declaration that can be attached to a class declaration, method, accessor, property, or parameter. Decorators use...
Read more >
How To Use Decorators in TypeScript - DigitalOcean
Decorators are a way to decorate members of a class, or a class itself, with extra functionality. This tutorial covers creating decorators ......
Read more >
A Complete Guide to TypeScript Decorators - Disenchanted
There are five types of decorators we can use: Class Decorators; Property Decorators; Method Decorators; Accessor Decorators; Parameter ...
Read more >
How do I make a decorator typesafe in TypeScript?
Check this if it work for you interface ClassType<T> extends Function { new (...args: any[]): T; } type MethodKeys<T> = ({ [K in...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found