Safe decorator implementation with ^ type operator.
See original GitHub issueOutline
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 decorator
s. 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:
- Created 3 years ago
- Reactions:15
- Comments:8 (1 by maintainers)
Top GitHub Comments
It seems like what you want is actually not decorators (which are a runtime feature) but in fact some kind of branded type?
@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 adecorator
function who is using the^
type operator, the target variable’s type would be defined implicitly like below: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 movedecorator
feature fromexperimental
to standard.If you’ve an idea about the safe decorator, let’s argue about it.