Description
openedon Sep 4, 2015
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:
interface MyInterface {
prop: number;
}
let val: typeon MyInterface.prop; // type of val is 'number'
This would work with any property, including ones contained in anonymous and nested interfaces:
interface MyInterface {
obj: {
x: number;
y: number;
}
}
let myX: typeon MyInterface.obj.x; // type of myX is now 'number'
Generic interfaces:
interface MyInterface<T> {
obj: {
x: T;
y: T;
func: (arg: T) => T;
}
}
let f: typeon MyInterface<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:
interface MyInterface {
val: string;
anotherVal: typeon MyInterface.val;
obj: {
x: number,
y: number;
func(arg:number): typeon MyInterface.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:
interface MyInterface {
val: string;
anotherVal: typeon this.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): typeon this.x;
}
}
It would also be available in interfaces declared through type
declarations and for purely anonymous ones:
type MyType = {
name: string;
age: number;
query: (username: typeon this.name) => typeon this.age;
}
let test: { x: number, y: typeon this.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
):
class UserBase {
id: string;
}
class User extends UserBase {
age: number;
chatWithUser(id: typeon this.id): { id: typeon User.id, age: typeon User.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).
class Example {
val: number;
static val: string;
instanceFunc1(): typeon this.val; // OK, resolves to 'number'
instanceFunc2(): typeof Example.val; // OK, resolves to 'string'
static staticFunc1(): typeon Example.val; // OK, consistent with external references.
static staticFunc2(): typeof Example.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:
class Example<T> { // The example is given where T = number
val: T;
static val: string;
instanceFunc1(): typeon this.val; // OK, resolves to 'number'.
instanceFunc2(): typeof Example.val; // OK, resolves to 'string'
static staticFunc1(): typeon Example<number>.val; // OK, consistent with external references.
static staticFunc2(): typeof Example.val; // OK, resolves to 'string'
}
let c = new Example<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:
class UserBase {
// 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;
}
class User extends UserBase {
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: typeon this.id): { id: typeon User.id, age: typeon User.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:
class Process {
id: number;
state: {
memoryUsage: {
real: number;
virtual: number;
private: number;
};
processorUsage: Array<{ index: number, percentage: number }>;
}
constructor() {
...
}
..
}
class OSQuery {
static queryMemoryUsage(id: typeon Process.id): typeon Process.state.memoryUsage {
return {
real: OS.getRealUsage(id),
virtual: OS.getVirtualUsage(id),
private: OS.getPrivateUsage(id)
};
}
static queryProcessorsUsage(id: typeon Process.id): typeon Process.state.processorUsage {
result: typeon Process.state.processorUsage;
for (let i=0; i< OS.processorCount; i++) {
result.push( { index: i, percentage: OS.getProcessorUsage(id, i) })
}
return result;
}
static queryProcessState(id: typeon Process.id): typeon Process.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:
interface ProcessorUsageEntry {
index: number;
percentage: number;
}
interface MemoryUsage {
real: number;
virtual: number;
private: number;
}
type ProcessorID = number; // aliased in case that associated type changes
interface ProcessState {
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):
class MyClass {
instanceProp: number;
static staticProp: string;
constructor(arg: boolean) {
..
}
}
let a: typeof MyClass; // 'a' now has type { staticProp: string, new (arg: boolean) }
let b: typeon MyClass; // 'b' now has type { instanceProp: number }
Equivalent reduction to named type aliases
This can be internally implemented in the compiler like this:
Source:
interface MyInterface {
val1: string;
val2: typeon this.val1;
val3: typeon this.val2;
obj: {
x: number,
y: number;
func(arg: string): typeon this.x;
}
}
Reduction:
type TypeOn_val1 = string;
type TypeOn_val2 = TypeOn_val1;
type TypeOn_val3 = TypeOn_val2;
type TypeOn_obj_x = number;
type TypeOn_obj_func_returnType = TypeOn_obj_x;
type TypeOn_obj = {
x: TypeOn_obj_x;
y: number;
func(arg: string): TypeOn_obj_func_returnType;
}
interface MyInterface {
val1: TypeOn_val1;
val2: TypeOn_val2;
val3: TypeOn_val3;
obj: TypeOn_obj;
}
Example with generic interfaces:
Source:
interface MyGenericInterface<T> {
val1: T;
val2: typeon val1;
obj: {
a: Array<T>,
func(arg: T): typeon this.a;
}
}
Reduction (this uses the new generic type alias declarations introduced in 1.6):
type TypeOn_val1<T> = T;
type TypeOn_val2<T> = TypeOn_val1<T>;
type TypeOn_obj_a<T> = Array<T>;
type TypeOn_obj_func_returnType<T> = TypeOn_obj_a<T>;
type TypeOn_obj<T> = {
a: TypeOn_obj_a<T>;
func(arg: T): TypeOn_obj_func_returnType<T>
};
interface MyGenericInterface<T> {
val1: TypeOn_val1<T>;
val2: TypeOn_val2<T>;
obj: TypeOn_obj<T>;
}
Possible issues and their solutions
Detect and error on circular references:
interface SelfReferencingPropertyType {
x: typeon this.x; // Error: self reference
}
interface EndlessLoop {
x: typeon this.y; // Error: indirect self reference
y: typeon this.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:
interface MyInterface {
obj: {
x: number,
y: number
}
}
let dummy: MyInterface;
let val: typeof dummy.obj;
And even:
interface MyInterface {
obj: {
x: number,
y: typeof dummy.obj.x;
}
}
interface AnotherInterface {
func(arg:number): typeof dummy.obj;
}
var dummy: 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:
interface MyInterface<T> {
x: T;
y: typeon this.x; // Not possible to emulate this
}
[Originally described at #4555]