Skip to content

Safe decorator implementation with ^ type operator. #38620

Description

@samchon

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()exportclassBbsArticleextendsModel<BbsArticle>{
@Column("varchar",{restrict: ["NORMAL","NOTICE","QUESTION","SUGGESTION"],default: "NORMAL"})publiccategory!: "NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION";
@Column("varchar")publictitle!: number;
@Column("varchar",{nullable: true})publicsub_title!: string|null;
@Column("text")publiccontent!: boolean;
@Column("int",{unsigned: true,default: 1})publichits!: number;}functionColumn<TypeextendsTypeList,OptionsextendsOptions<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()exportclassBbsArticleextendsModel<BbsArticle>{// ("NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION") ^ SomeFunctionpubliccategory!: @Column("varchar",{restrict: ["NORMAL","NOTICE","QUESTION","SUGGESTION"],default: "NORMAL"});// string ^ SomeFunctionpublictitle!: @Column("varchar");// (string | null) ^ SomeFunctionpublicsub_title!: @Column("varchar",{nullable: true});// string ^ SomeFunctionpubliccontent!: @Column("text");// number ^ SomeFunctionpublichits!: @Column("int",{unsigned: true,default: 1});}functionColumn<TypeextendsTypeList,OptionsextendsOptions<Type>>(type: Type,options?: Options): DeductType<Type,Options>^SomeFunction;

Pseudo type operator ^

// Decorator is a function returning a function.functionSomeDecorator(): Function;// Variable type cannot be expressedvarsomeVariable: @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.

functionSomeDecorator(): number^Function{returnfunction(){// implementation code};}typeSomeType=ReturnType<SomeDecorator>;// TYPE EXTENSIONS ARE ALL POSSIBLEtypeExtendsNumber=SomeTypeextendsnumber ? true : false;// truetypeExtendsColumn=SomeTypeextendsFunction ? true : false;// true// ASSIGNING VALUE IS POSSIBLEletx: SomeType=3;// no error// ASSIGNING THE DECORATOR FUNCTION IS NOT POSSIBLEletdecorator: 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()exportclassBbsArticleextendsModel<BbsArticle>{/* ----------------------------------------------------------- COLUMNS ----------------------------------------------------------- */// number ^ IncrementalColumn<"int">publicreadonlyid!: @IncrementalColumn("int");// number ^ ForeignColumn<ForeignColumn>publicbbs_group_id!: @ForeignColumn(()=>BbsGroup);// (number | null) ^ ForeignColumn<BbsArticle, Options>publicparent_article_id!: @ForeignColumn(()=>BbsArticle,{index: true,nullable: true});// ("NORMAL"|"NOTICE"|"QUESTION"|"SUGGESTION") ^ Column<"int", Options>publiccategory!: @Column("varchar",{restrict: ["NORMAL","NOTICE","QUESTION","SUGGESTION"],default: "NORMAL"});// string ^ Column<"varchar", Options>publicwriter!: @Column("varchar",{index: true,default: ()=>Random.characters(16)});// string ^ Column<"varchar">publicpassword!: @Column("varchar");// string ^ Column<"varchar">publictitle!: @Column("varchar");// (string | null) & Column<"varchar", Options>publicsub_title!: @Column("varchar",{nullable: true});// string ^ Column<"text">publiccontent!: @Column("text");// number ^ Column<"int", Options>publichits!: @Column("int",{unsigned: true,default: 0});// Date ^ CreationTimeColumnpubliccreated_at!: @CreationTimeColumn();// (Date | null) ^ UpdationTimeColumnpublicupdated_at!: @UpdationTimeColumn();// (Date | null) ^ SoftDeletionColumnpublicdeleted_at!: @SoftDeletionColumn();/* ----------------------------------------------------------- RELATIONSHIPS ----------------------------------------------------------- */publicgetGroup(): Promise<BbsGroup>{returnthis.belongsTo(BbsGroup,"bbs_group_id");}publicgetParent(): Promise<BbsArticle|null>{returnthis.belongsto(BbsArticle,"parent_article_id");}publicgetChildren(): Promise<BbsArticle[]>{returnthis.hasMany(BbsArticle,"parent_article_id");}publicgetTags(): Promise<BbsTag[]>{returnthis.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:

exportabstractclassModel<EntityextendsModel<Entity>>{protectedasyncbelongsTo<TargetextendsModel<Target>,FieldextendsSpecialFields<Entity,ForeignColumn<Target>>>(target: CreatorType<Target>,field: Field): Promise<Entity[Field]extendsForeignColumn<Target,{nullable: true}>
? Target|null
: Target>;protectedasynchasOne<TargetextendsModel<Target>,FieldextendsSpecialFields<Target,ForeignColumn<Entity>>,Symmetricextendsboolean>(target: ObjectType<Target>,field: Field,symmetric: Symmetric): Promise<Symmetricextendstrue ? Target : Target|null>;protectedasynchasMany<TargetextendsModel<Target>,FieldextendsSpecialFields<Target,ForeignColumn<Entity>>>(target: ObjectType<Target>,field: Field): Promise<Target[]>;// 1: M: N => 1: XprotectedhasManyToMany<TargetextendsModel<Target>,RouteextendsModel<Route>,TargetFieldextendsSpecialFields<Route,ForeignColumn<Target>>,MyFieldextendsSpecialFields<Route,ForeignColumn<Entity>>>(target: ObjectType<Target>,route: ObjectType<Route>,targetField: TargetField,myField: MyField): Promise<Target[]>;// M: N => 1: M: 1protectedhasManyThrough<TargetextendsModel<Target>,RouteextendsModel<Route>,TargetFieldextendsSpecialFields<Target,ForeignColumn<Route>>,RouteFieldextendsSpecialFields<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"{exportclassBaseEntity<EntityextendsBaseEntity<Entity>>{// NO INITIALIZER CONSTRUCTOR EXISTSpublicconstructor();// RECORDS ARE DANGEROUS (PARTIAL) TYPE// AS CANNOT DISTINGUISH WHETHER ESSENTIAL OR OPTINALpublicstaticinsert<EntityextendsBaseEntity<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.

exportclassModel<EntityextendsModel<Entity>>{publicstaticinsert<EntityextendsModel<Entity>>(this: CreatorType<Entity>,records: Model.Props<Entity>[]): Promise<Entity[]>;/** * Initializer Constructor *  * @param props Properties would be assigned to each columns */publicconstructor(props: Model.IProps<Entity>);}exportnamespaceModel{exporttypeProps<EntityextendsModel<Entity>>=OmitNever<RequiredProps<Entity,true>>&Partial<OmitNever<RequiredProps<Entity,false>>>;typeRequiredProps<EntityextendsModel<Entity>,Flagextendsboolean>={[PinkeyofEntity]: Entity[P]extendsColumnBase<infer Name, infer Options>
? IsRequired<Entity[P],Name,Options>extendsFlag
? ColumnBase.Type<Name,Options>
: never
: never};typeIsRequired<ColumnextendsColumnBase<Name,Options>,NameextendskeyofColumnBase.TypeList,Options>=ColumnextendsIncrementalColumn<any> ? false
: ColumnextendsUuidColumn<any> ? false
: OptionsextendsINullable<true> ? false
: OptionsextendsIDefault<any> ? false
: true;}typeModel.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")exportclassBbsArticlesController{
@Get()publicindex(httpReq: @HttpRequest(),group: @Param("group","string"),input: @Query<IPage.IRequest>()): Promise<IPage<IArticle.ISummary>[]>;
@Get(":id")publicasyncat(httpReq: @HttpRequest(),group: @Param("group","string"),id: @Param("id","number")): Promise<IArticle>;
@Post()publicasyncstore(httpReq: @HttpRequest(),group: @Param("group","string"),input: @RequestBody<IArticle>()): Promise<IArticle>;
@Put(":id")publicasyncupdate(httpReq: @HttpRequest(),group: @Param("group","string"),id: @Param("id","number"),input: @RequestBody<Partial<IArticle>>()): Promise<IArticle>;}

Metadata

Metadata

Assignees

No one assigned

    Labels

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions