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 errorAppendix
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>;}
Outline
Nowadays, lots of famous JavaScript libraries like
typeormornestjsare supporting thedecoratorfeature. However, thedecoratorfeature is not suitable for core philosophy of the TypeScript; the safe implementation.When defining a
decoratoronto 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
titleandcontentarevarcharandtext. However, their instance types arenumberandboolean. Their instance type must bestring, but there would not be any compile error when using the TypeScript. It's the dangerous characteristic of thedecoratorwhat I want to say.I think such dangerous characteristic is the reason why TypeScript is supporting the
decoratorasexperimentalfeature for a long time. I want to suggest an alternative solution that can makedecoratorto be much safer, so that TypeScipt can adapt thedecoratoras 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.Pseudo type operator
^In JavaScript,
decoratoris a type of function returning a meta function. In the ordinary TypeScript, thedecoratorfunction would be represented like upper code. Therefore, there's no way to express the variable type who're using thedecorator.Therefore, I suggest a new pseudo type operator,
^symbol. With the^symbol, expressing both variable anddecoratortypes, 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 targetdecorator.^RightWithin 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.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:If this issue be adopted, so that the safe
decoratorimplementation 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:Also, the safe
decoratorcan make intializer construction much safer, too.In the case of
typeorm, usingstricttype checking options are discouraged. It's because the olddecoratorcan't express the target variable's detailed options likenullableor auto-assigneddefaultvalue. Therefore, in the case oftypeorm, initializer constructor is not supported. Even massive insertion methods are using the dangerousPartialtype, because it can't distinguish which field is whether essential or optional.However, if safe
decoratorimplementation with^type operator is realized, supporting intializer constructor and massive insertion method with exact type are possible. Asdecoratordefining each column contains the exact type information, distinguishing whether which field is essential or optional.API Controllers
In nowadays, many JavaScript libraries like nestjs are wrapping express framework with
decoratorfor convenient. However, wrapping features of express components withdecorator, it loses chance to detecting m is-type-usage error in the compile level.However, if safe
decoratorimplementation 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 safedecoratormeans: