Overview
There are some holes in the TS type system that's really hard to cover. Since, special casing syntaxes for a specific use case is not very efficient, nonetheless a system with a lot of special cases is not a good system. Here are some current proposals, of special cases with special syntaxes in the TS issue tracker:
But, I have also had a very strong desire to strongly type an ORM(Object Relational Mapping) and strongly type data models, where I don't need to repeat the code a lot. Below, is a real world case where, I use the popular ORM, sequelize, to define a comment model:
interfaceModelextendsId,UpdatedAt,CreatedAt{}interfaceACommentextendsModel{text: string;userId?: number;postId?: number;}exportinterfaceICommentextendsAComment{user?: IUser;}exportconstComment=DbContext.define<IComment,AComment>('comment',{text: {type: Type.TEXT,allowNull: false,createdAt: false,updatedAt: true},});// In another fileconstcomment=Comment.create({text: 'helloword',postId: 1,userId: 1,}// Must be assignable to 'AComment');comment;// ICommentAs you can see, it is quite tedious, to have to write just one model. I need to define a type for the argument side,AComment, and one for the instance side of model, IComment, and at the end I need to send in the values to the factory function to produce a SQL table for that model. What I don't understand is, we have all the types inferred from the factory function's argument. Why can't we get the type from there to produce AComment and IComment?
Proposal
I propose a solution, I call custom type operator. So in layman terms, it takes a couple of type arguments and produces a new type:
typeDbArgument<FextendsFields>=>{typeType;// Do something with the TypereturnType}In below, we added two custom type operator DbArgument<F> and DbInstance<F>, so that we can skip to write IComment and AComment in the previous example.
namespaceType{exportconstInteger: {__integer: any};exportconstString(length: number)=>{return{
length,}as{__string?: any,length: number};};}interfaceField{type: typeofType.Integer|typeofType.String(number);allowNull: boolean;createdAt: boolean;updatedAt: boolean;primaryKey: boolean;autoIncrement: boolean;}interfaceFields{[field: string]: Field;}typeDbInstance<FextendsFields>=>{typeType;for(KinF){// Loop through all fields of FtypeField=F[K];if(Field.type=={__integer: any}){Type[K]: number;}else{Type[K]: string;}if(Field.allowNull==true){Type|=null;}if(Field.createdAt==true){Type&={createdAt: string};}if(Field.updatedAt==true){Type&={updatedAt: string};}}returnType;}typeDbArgument<FextendsFields>=>{typeType;for(KinF){// Loop through all fields of FtypeField=F[K];typeValueType;if(Field.type=={__integer: any}){ValueType=number;}else{ValueType=string;}if(Field.required==true){Type[K]: ValueType;}else{Type[K?]: ValueType;}}returnType;}namespaceDbContext{interfaceDbModel<F>{create(DbArgument<F>): Promise<DbInstance<F>>}exportfunctiondefine<FextendsFields>(fields: F): DbModel<F>{}}constComment=DbContext.define('comment',{id: {type: Type.Integer,autoIncrement: true,primaryKey: true},text: {type: Type.String(50),allowNull: false,createdAt: false,updatedAt: true,required: true},});constcomment=awaitComment.create({text: 'helloworld'}// Conform to DbArgument<F>);comment;// DbInstance<F> where F is { text: string }With the value argument we produced a type for the instance side IComment and one for the argument side AComment. And all the user have to write is the argument values to the factory function define:
constComment=DbContext.define('comment',{id: {type: Type.Integer,autoIncrement: true,primaryKey: true},text: {type: Type.String(50),allowNull: false,createdAt: false,updatedAt: true,required: true},});You can regard it as the equivalent to a function in the value space. But instead of the value space it operates in the type space.
Syntax
Definition
A Custom Type Operator's body need to return a type and can never reference a value.
Example
typeIdentity<T>={returnT;}Grammar
TypeOperatorFunction :
`type` `=` Identifier TypeParameters `=>` TypeOperatorFunctionBody
TypeParameters : `<` TypeParameterList `>`
TypeParameterList : TypeParameter
TypeParameterList `,` TypeParameter
TypeParameter :
Identifier
Identifier `extends` Identifier
`...`Identifier
TypeOperatorFunctionBody :
`{` DeclarationsStatmentsAndExpressions `} `
Type Relations
A == B returns true if B is invariant to A else false.A < B returns true if B is a sub type of A else false.A <= B returns true if B is a sub type of A or invariant else false.A > B returns true if B is a super type of A else false.A >= B returns true if B is a super type of A or invariant else false.A != B returns true if B is not invariant to A else false.case A (switch (T)) the same as T == A.T is falsy if it contains the types '', 0, false, null, undefined.T is truthy when it is not falsy.
Assignment Operators
A &= B is equal to A = A & B;A |= B is equal to A = A | B;A['k']: string is equal to A = A & { k: string };A['k'?]: string is equal to A = A & { k?: string };A[readonly 'k']: string is equal to A = A & { readonly k: string };
If Statement
Example
Grammar
IfStatement :
`if` `(` IfCondition `)` IfStatementBody
IfStatementBody : `{` DeclarationsStatmentsAndExpressions `}`
Switch Statement
A case statement in a switch statement, evaluates in the same way as the binary type operator ==
Example
switch(T){case'1':
returnnumber;case'a':
case'b':
returnstring;default:
Type|=null;}Grammar
SwitchStatement :
`switch` `(` Type `)` SwitchStatementBody
SwitchStatementBody : `{` CaseDefaultStatements `}`
CaseDefaultStatements
CaseStatements
`default` `:` CaseStatements : `case` Type `:` DeclarationsStatmentsAndExpressions
CaseStatements `case` Type `:` DeclarationsStatmentsAndExpressions
SwitchCaseBody :
DeclarationsStatmentsAndExpressions
DeclarationsStatmentsAndExpressions `break`
For-In Statement
Loop through each property in a type.
Example
Grammar
ForInStatement :
`for` `(` Identifier `in` Type `)` ForStatementBody
ForStatementBody :
DeclarationStatmentsAndExpressions
For-of Statement
Loop through an array / tuple.
Example
Grammar
ForOfStatement :
`for` `(` Identifier `of` Type `)` ForStatementBody
ForStatementBody :
DeclarationStatmentsAndExpressions
Examples
DeepPartial
typeDeepPartial<T>=>{typeType;for(KinT){if(T[K]==boolean||T[K]==string||T[K]==null||T[K]==number){Type[K?]=T[K];}else{Type[K?]=DeepPartial<T[K]>;}}returnType;}Rest
typeRest<T, ...Sextendsstring[]>=>{typeType;for(KTinT){typeIsInS=false;for(KSofS){if(S[KT]!=undefined){IsInS=true;}}if(!IsInS){Type[KT]=T[KT]}}returnType;}cc Nathan Shively-Sanders (@sandersn)
Overview
There are some holes in the TS type system that's really hard to cover. Since, special casing syntaxes for a specific use case is not very efficient, nonetheless a system with a lot of special cases is not a good system. Here are some current proposals, of special cases with special syntaxes in the TS issue tracker:
deepPartialto deep optionalize a type Partial Types (Optionalized Properties for Existing Types) #4889.restto get the rest part of a type Rest type #13470.But, I have also had a very strong desire to strongly type an ORM(Object Relational Mapping) and strongly type data models, where I don't need to repeat the code a lot. Below, is a real world case where, I use the popular ORM, sequelize, to define a comment model:
As you can see, it is quite tedious, to have to write just one model. I need to define a type for the argument side,
AComment, and one for the instance side of model,IComment, and at the end I need to send in the values to the factory function to produce a SQL table for that model. What I don't understand is, we have all the types inferred from the factory function's argument. Why can't we get the type from there to produceACommentandIComment?Proposal
I propose a solution, I call custom type operator. So in layman terms, it takes a couple of type arguments and produces a new type:
In below, we added two custom type operator
DbArgument<F>andDbInstance<F>, so that we can skip to writeICommentandACommentin the previous example.With the value argument we produced a type for the instance side
ICommentand one for the argument sideAComment. And all the user have to write is the argument values to the factory functiondefine:You can regard it as the equivalent to a function in the value space. But instead of the value space it operates in the type space.
Syntax
Definition
A Custom Type Operator's body need to return a type and can never reference a value.
Example
Grammar
Type Relations
A == BreturnstrueifBis invariant toAelsefalse.A < BreturnstrueifBis a sub type ofAelsefalse.A <= BreturnstrueifBis a sub type ofAor invariant elsefalse.A > BreturnstrueifBis a super type ofAelsefalse.A >= BreturnstrueifBis a super type ofAor invariant elsefalse.A != BreturnstrueifBis not invariant toAelsefalse.case A(switch (T)) the same asT == A.Tisfalsyif it contains the types'',0,false,null,undefined.Tistruthywhen it is notfalsy.Assignment Operators
A &= Bis equal toA = A & B;A |= Bis equal toA = A | B;A['k']: stringis equal toA = A & { k: string };A['k'?]: stringis equal toA = A & { k?: string };A[readonly 'k']: stringis equal toA = A & { readonly k: string };If Statement
Example
Grammar
Switch Statement
A case statement in a switch statement, evaluates in the same way as the binary type operator
==Example
Grammar
For-In Statement
Loop through each property in a type.
Example
Grammar
For-of Statement
Loop through an array / tuple.
Example
Grammar
Examples
DeepPartial
Rest
cc Nathan Shively-Sanders (@sandersn)