Skip to content

Custom Type Operator #13500

Description

@tinganho

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;// IComment

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 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

if(A<B){}

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

for(KinP){}

Grammar

ForInStatement :
`for` `(` Identifier `in` Type `)` ForStatementBody
ForStatementBody :
DeclarationStatmentsAndExpressions

For-of Statement

Loop through an array / tuple.

Example

for(SofSs){}

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Out of ScopeThis idea sits outside of the TypeScript language design constraintsToo ComplexAn issue which adding support for may be too complex for the value it adds

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions