Skip to content

Proposal: 'typeon' operator #4640

Description

@rotemdan

Proposal: 'typeon' operator

The typeon operator references the type associated with an interface or class property, but in contrast to typeof, refers to it through the containing Type itself. I.e. directly queries the type set on the property rather than the type of an instance of it:

interfaceMyInterface{prop: number;}letval: typeonMyInterface.prop;// type of val is 'number'

This would work with any property, including ones contained in anonymous and nested interfaces:

interfaceMyInterface{obj: {x: number;y: number;}}letmyX: typeonMyInterface.obj.x;// type of myX is now 'number'

Generic interfaces:

interfaceMyInterface<T>{obj: {x: T;y: T;func: (arg: T)=>T;}}letf: typeonMyInterface<number>.obj.func;// type of f is now (arg: number) => number

It would be possible to reference the property types at any scope, including within the referenced declaration itself:

interfaceMyInterface{val: string;anotherVal: typeonMyInterface.val;obj: {x: number,y: number;func(arg:number): typeonMyInterface.obj.x;}}

The this type reference

A special this type reference could be supported for this very purpose. this would be scoped to the closest containing interface, and that would also apply to anonymous ones:

interfaceMyInterface{val: string;anotherVal: typeonthis.val;obj: {x: number,y: number;// Note: 'this' is scoped to the anonymous interface here. // 'typeon this.val' would give an error.func(arg: string): typeonthis.x;}}

It would also be available in interfaces declared through type declarations and for purely anonymous ones:

typeMyType={name: string;age: number;query: (username: typeonthis.name)=>typeonthis.age;}lettest: {x: number,y: typeonthis.x}

and to classes, which would also include references to the types of properties declared in base classes when using this (a possible extension could also include support for super):

classUserBase{id: string;}classUserextendsUserBase{age: number;chatWithUser(id: typeonthis.id): {id: typeonUser.id,age: typeonUser.age}{// The first argument for 'chatWith' is expected to be a string here, so this works.chatWith(id);..}}

In a class declaration, typon this can only be used from instance positions. To reference static members, the name of the class would be used with typeof (note: no equivalent typeof this or a standalone this type is included in this proposal).

classExample{val: number;staticval: string;instanceFunc1(): typeonthis.val;// OK, resolves to 'number'instanceFunc2(): typeofExample.val;// OK, resolves to 'string'staticstaticFunc1(): typeonExample.val;// OK, consistent with external references.staticstaticFunc2(): typeofExample.val;// OK, resolves to 'string'}

This was chosen to improve clarity and for consistency with generic classes, where the instantiated values of generic parameters are not available at static positions:

classExample<T>{// The example is given where T = numberval: T;staticval: string;instanceFunc1(): typeonthis.val;// OK, resolves to 'number'.instanceFunc2(): typeofExample.val;// OK, resolves to 'string'staticstaticFunc1(): typeonExample<number>.val;// OK, consistent with external references.staticstaticFunc2(): typeofExample.val;// OK, resolves to 'string'}letc=newExample<number>();

Example use cases

Using these ad-hoc references would make it easier to express of the semantic intention for the usage of the type, and automatically "synchronize" with future changes to the type of the referenced property. This both reduces effort and prevents human errors:

classUserBase{// The type for 'id' has now changed, this usually means that all similar // semantic usages of it would need to be manually checked and updated, // this includes ones in derived classes.id: string|number;}classUserextendsUserBase{age: number;// The type of the 'id' argument and anonymous interface property has // automatically synchronized with the one set in the base class!// This ensures that they will always be consistent and will propagate type errors // into the body of the function.chatWithUser(id: typeonthis.id): {id: typeonUser.id,age: typeonUser.age}{// The first argument for 'chatWith' is expected to be a string here, // but now the type of 'id' has changed to string | number so will error.// This compile-time error is possible because the type was referenced with 'typeon'chatWith(id);// Error! type 'string' is not assignable from 'string | number'..}}

Another effective use is to encapsulate and reference anonymous types within a class or interface declaration without needing to declare additional type aliases or interfaces. This yield a different style of coding:

classProcess{id: number;state: {memoryUsage: {real: number;virtual: number;private: number;};processorUsage: Array<{index: number,percentage: number}>;}constructor(){
...
}..}classOSQuery{staticqueryMemoryUsage(id: typeonProcess.id): typeonProcess.state.memoryUsage{return{real: OS.getRealUsage(id),virtual: OS.getVirtualUsage(id),private: OS.getPrivateUsage(id)};}staticqueryProcessorsUsage(id: typeonProcess.id): typeonProcess.state.processorUsage{
result: typeonProcess.state.processorUsage;for(leti=0;i<OS.processorCount;i++){result.push({index: i,percentage: OS.getProcessorUsage(id,i)})}returnresult;}staticqueryProcessState(id: typeonProcess.id): typeonProcess.state{return{memoryUsage: this.queryMemoryUsage(id),processorUsage: this.queryProcessorsUsage(id)}}..}

In the conventional style of coding, 4 auxiliary interfaces or type aliases would need to be declared to achieve this:

interfaceProcessorUsageEntry{index: number;percentage: number;}interfaceMemoryUsage{real: number;virtual: number;private: number;}typeProcessorID=number;// aliased in case that associated type changesinterfaceProcessState{id: ProcessorID;memoryUsage: MemoryUsage;processorUsage: Array<ProcessorUsageEntry>}

Reusing the syntax to reference a class instance type

It is also possible to naturally extend the syntax to reference a type that only includes members of a class instance (typeof MyClass only gives the type of the constructor):

classMyClass{instanceProp: number;staticstaticProp: string;constructor(arg: boolean){..}}leta: typeofMyClass;// 'a' now has type { staticProp: string, new (arg: boolean) }letb: typeonMyClass;// 'b' now has type { instanceProp: number }

Equivalent reduction to named type aliases

This can be internally implemented in the compiler like this:

Source:

interfaceMyInterface{val1: string;val2: typeonthis.val1;val3: typeonthis.val2;obj: {x: number,y: number;func(arg: string): typeonthis.x;}}

Reduction:

typeTypeOn_val1=string;typeTypeOn_val2=TypeOn_val1;typeTypeOn_val3=TypeOn_val2;typeTypeOn_obj_x=number;typeTypeOn_obj_func_returnType=TypeOn_obj_x;typeTypeOn_obj={x: TypeOn_obj_x;y: number;func(arg: string): TypeOn_obj_func_returnType;}interfaceMyInterface{val1: TypeOn_val1;val2: TypeOn_val2;val3: TypeOn_val3;obj: TypeOn_obj;}

Example with generic interfaces:
Source:

interfaceMyGenericInterface<T>{val1: T;val2: typeonval1;obj: {a: Array<T>,func(arg: T): typeonthis.a;}}

Reduction (this uses the new generic type alias declarations introduced in 1.6):

typeTypeOn_val1<T>=T;typeTypeOn_val2<T>=TypeOn_val1<T>;typeTypeOn_obj_a<T>=Array<T>;typeTypeOn_obj_func_returnType<T>=TypeOn_obj_a<T>;typeTypeOn_obj<T>={a: TypeOn_obj_a<T>;func(arg: T): TypeOn_obj_func_returnType<T>};interfaceMyGenericInterface<T>{val1: TypeOn_val1<T>;val2: TypeOn_val2<T>;obj: TypeOn_obj<T>;}

Possible issues and their solutions

Detect and error on circular references:

interfaceSelfReferencingPropertyType{x: typeonthis.x;// Error: self reference}interfaceEndlessLoop{x: typeonthis.y;// Error: indirect self referencey: typeonthis.x;// Error: indirect self reference}

This would happen automatically through the reduction described above. The error currently reported through type is "Error: type 'TypeOn_EndlessLoop_y' circularly references itself".

Current workarounds

It is currently possible to partially emulate this using typeof with a "dummy" instance of the type:

interfaceMyInterface{obj: {x: number,y: number}}letdummy: MyInterface;letval: typeofdummy.obj;

And even:

interfaceMyInterface{obj: {x: number,y: typeofdummy.obj.x;}}interfaceAnotherInterface{func(arg:number): typeofdummy.obj;}vardummy: MyInterface;

Though these workarounds requires a globally scoped variable, and not always possible or desirable for use in declaration files. They also cannot support keywords like this (or super) thus cannot be used within anonymous types.

References to generic parameters cannot be emulated:

interfaceMyInterface<T>{x: T;y: typeonthis.x;// Not possible to emulate this}

[Originally described at #4555]

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already createdNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.SuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions