Skip to content

Proposal: 'typeon' operator #4640

Closed
Closed

Description

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]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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