Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Proposal: 'typeon' operator #4640

Closed
rotemdan opened this issue Sep 4, 2015 · 19 comments
Closed

Proposal: 'typeon' operator #4640

rotemdan opened this issue Sep 4, 2015 · 19 comments
Labels
Duplicate An existing issue was already created Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript

Comments

@rotemdan
Copy link

rotemdan commented 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]

@DanielRosenwasser DanielRosenwasser added the Suggestion An idea for TypeScript label Sep 4, 2015
@danquirk
Copy link
Member

danquirk commented Sep 4, 2015

Can the typeof operator not be extended to support these cases?

@DanielRosenwasser
Copy link
Member

Can the typeof operator not be extended to support these cases?

I believe it would be ambiguous for classes:

class C {
    static prop;
    prop;
}

var p: typeof C.prop;

@kitsonk
Copy link
Contributor

kitsonk commented Sep 5, 2015

Obviously introducing another keyword is icky, especially for a bit of an edge case. For the class use case, the following would seem logical to me:

class C {
    static prop: number;
    prop: string;
}

let p: typeof C.prop = 'foo'; // p typeof string
let q: typeof typeof C.prop = 1; // q typeof number

@rotemdan
Copy link
Author

rotemdan commented Sep 5, 2015

A more practical example to demonstrate its real-world usefulness:

Encapsulate and reference anonymous types (through the equivalent of "auto-aliasing") in a class or interface without needing to declare additional external type aliases or interfaces. This allow 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 here: ProcessID, ProcessState, ProcessMemoryUsage, ProcessProcessorUsage.

@benliddicott
Copy link

@DanielRosenwasser , @danquirk:

Actually you can do these type deductions already, using a type inference hack. That means the type system can already do it, so it is really syntactic support which is required:

class MyClass {
    instanceProp: number;
    static staticProp: string;

    constructor(arg: boolean) {
       // ..
    }
}


    function func<T>(a: T): { a: T } { return { a: a }; };

    // a has type string. This works today using typeof syntax.
    let a: typeof MyClass.staticProp = "";

    // Typeof doesn't work with instance properties, but type inference can still deduce the type.
    // b has the type number.
    let b = false ? (<MyClass>(void 0)).instanceProp : void 0;

    // c has type {a: number}. 
    // Again, type inference can deduce the type without evaluating the expression
    let c = false ? (func(<number>(void 0))) : void 0;


Proposal: Allow arbitrary expressions as argument to typeof. These "type expressions" would not be evaluated, only the type deduction would take place at compile time.

If typeof accepted arbitrary expressions instead of identifiers, this would meet the need, I think.

class MyClass {
    instanceProp: number;
    static staticProp: string;

    constructor(arg: boolean) {
       // ..
    }
}


    function func<T>(a: T): { a: T } { return { a: a }; };

// b has type of the instanceProp property of MyClass.
let b: typeof (<MyClass>(void 0)).instanceProp; 
// c has same type as the return value of func(1), (but func is not evaluated).
let c: typeof (func(<number>(void 0))); 

Further proposal: Allow (<typename>) as a shorthand for (<typename>(void 0)) within a type expression. This would simplify writing type expressions for such uses as @rotemdan describes. so typeof func(<number> 10).a could be written typeof func(<number>).a

@yortus
Copy link
Contributor

yortus commented Sep 10, 2015

Proposal: Allow arbitrary expressions as argument to typeof. These "type expressions" would not be evaluated, only the type deduction would take place at compile time.

I agree with this approach. It solves all the cases mentioned here. It handles anonymous types. It's not ambiguous with classes:

class C {
    static prop;
    prop;
}

var pStatic: typeof C.prop;
var pInstance: typeof new C().prop;

And the compiler can already do this, it just doesn't expose the ability. This would just remove its limitations.

C++ has exactly this operator, it is called decltype and it maps an expression to a type without evaluating the expression. Given the limitations currently placed on typeof, it could be extended to work just like decltype without a breaking change. It would become far more useful.

@rotemdan
Copy link
Author

@yortus, @benliddicott

A main goal and benefit of the approach I presented is to allow a clean and natural way to reference into nested anonymous types like is demonstrated in this previous comment. Without a concise and readable syntax that would not generally be a practical thing to do. It was designed with the full intention of becoming a mainstream and usable pattern, not just an "advanced" feature or a trick which would be mostly known and used by experienced developers.

Another benefit is the addition of the this "type", which for generic types would also mean transparently propagating the generic parameter(s) of the current scope to get an even cleaner syntax:

class Process<T> {
  options: {
    priority: number;
    type: T;
    relatedProcesses: {
      parentProcess: typeon this;
      subProcessess: Array<typeon this>;
    }
  }
  constructor(options: typeon this.options) {
    ..
  }
}

Using typeof expression, it would look like this:

class Process<T> {
  options: {
    priority: number;
    type: T;
    related: {
      parentProcess: typeof (new Process<T>());
      subProcessess: Array<typeof (new Process<T>())>;
    }
  }
  constructor(options: typeof ((new Process<T>()).options)) {
    ..
  }
}

I understand the need for a more powerful operator, and using expressions would achieve that, but that wouldn't automatically make it more useful for these cases or for other more specific needs (such as returnTypeOf for example), so I think these are mostly unrelated proposals.

@yortus
Copy link
Contributor

yortus commented Sep 10, 2015

@rotemdan I think most people would write your last example like this:

class Process<T> {
    constructor(public options: ProcessOptions<T>) {
        ...
    }
}

interface ProcessOptions<T> {
    priority: number;
    type: T;
    relatedProcesses: {
        parentProcess: Process<T>;
        subProcessess: Array<Process<T>>;
    }
}

Is it really such a pain point to declare the ProcessOptions interface that a new syntax is needed? Note the other two uses of typeon in your example (inside the options interface) aren't needed at all.

@rotemdan
Copy link
Author

@yortus
You're right, I noticed it when I wrote it but I guess it was shown mostly to demonstrate how something equivalent would achieved specifically with that approach (and specifically for generic classes or interfaces), which at that case wasn't really that much better than just writing the names themselves (although still a bit shorter in some cases, especially for a long class/interface name).

The example in this comment better demonstrates the effect of being able to reference directly into nested anonymous types. With the common approach, every level of the type would need to be declared in its own interface or type alias, so instead of just stating:

state: {
  memoryUsage: {
    real: number;
    virtual: number;
    private: number;
  };

  processorUsage: Array<{ index: number, percentage: number }>;
}

The programmer would need to break it down to something like:

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

I wouldn't necessarily describe this as a "pain point", maybe something that most programmers feel is necessary as they aren't aware of alternative approaches (when appropriate, of course, I mean, not all cases benefit from this), maybe because this was never expressible in the language, and perhaps not in other ones as well (I'm not aware of other languages with an equivalent feature, but I guess I'm not deeply familiar with that many languages).

@yortus
Copy link
Contributor

yortus commented Sep 10, 2015

@rotemdan fair enough. I guess the deeper the anonymous nesting goes, the less difference in effort there is between

typeon Process.state.processorUsage

and

typeof new Process().state.processorUsage

@benliddicott
Copy link

@rotemdan , You are right to identify anonymous types as an important thing to enable.

I think the "typeof expression" approach can handle anonymous types - it's not quite as clean but almost. Instead of:

  • typeon Class.prop1.prop2.prop3

You would write one of the following:

  • typeof (<Class>{}).prop1.prop2.prop3 with no loss of function. Or alternatively
  • typeof new Class().prop1.prop2.prop3

In addition, it could be used in more ways than typeon. For example:

function Blah(): {a: number, b: string}{
return {a: 1, b: ""};
}

// Get the type of the return value.
var b: typeof (Blah());

It would also work to generate the correct type for the return value of a generic function, and so on.

All one has to do is construct an expression of the appropriate type. This can be done by writing an expression casting undefined to the desired type (or writing (new Type()) as @yortus does) then using the property accessors.

class Process {
    id: number;

    state: {
        memoryUsage: {
            real: number;
            virtual: number;
            private: number;
        };

        processorUsage: Array<{ index: number, percentage: number }>;
    }

    constructor() {
        //...
    }
    //...
}

class OSQuery {
    static queryMemoryUsage(id: typeof (<Process>{}).id): typeof (<Process>undefined).state.memoryUsage {
        return { 
            real: OS.getRealUsage(id), 
            virtual: OS.getVirtualUsage(id), 
            private: OS.getPrivateUsage(id)
        };
    }

    static queryProcessorsUsage(id: typeof (<Process>{}).id): typeof (<Process>undefined).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: typeof (<Process>{}).id): typeof (<Process>{}).state {
        return { 
            memoryUsage: this.queryMemoryUsage(id),
            processorUsage: this.queryProcessorsUsage(id)
        }
    }
    ..
}// Want the type of state.memoryUsage.virtual
// Has type number
var x: typeof ((<Blah>void 0).state.memoryUsage.virtual) = 0;


// want the type of state.processorUsage[]
// Has type {index: number, percentage: number}
var y: typeof ((<Blah>void 0).processorUsage[0]) = undefined;

@yortus
Copy link
Contributor

yortus commented Sep 10, 2015

// Get the type of the return value.
var b: typeof (Blah());

This would be a killer feature for me. This is an easily filled hole in TypeScript's otherwise great type inference abilities.

For instance, suppose you are writing a function, and you want it to take a parameter of a type, say QueryBuilder, that comes from a third-party library you are interoperating with, say Knex. Unfortunately QueryBuilder only appears as a return value from Knex's functions, and is anonymous.

You could make your own interface QueryBuilder {...} and use structural matching. It has lots of properties and methods so that's pretty painful.

You could modify knex.d.ts to expose its private QueryBuilder and submit a PR to DefinitelyTyped (which is what I did in the end).

Or you could write type QueryBuilder = typeof Knex().select(); and hey presto. I would like that very much.

If an anonymous type only appears in a return position, TypeScript doesn't make it easy to reuse it in an input position. In other words, TypeScript lacks an obvious way of expressing "hey this function's parameter has the same type as that function's return value", even though it can definitely do the inference required. typeof could easily express this and more.

@rotemdan
Copy link
Author

@benliddicott

I understand the need for a more powerful operator, but the main motivation here was to try to get a clean and readable syntax to achieve very specific purpose: allow to reference the types of properties by naturally "dotting" into the containing type, rather than through an instance of it, and to make that an easy and mainstream tool that is as approachable and usable for programmers as much as typeof is.

[I continued my comment at #4233 because it felt a bit off-topic here.]

@benliddicott
Copy link

OK I hadn't seen #4233. TBH, I think that's exactly what is wanted. You are asking for a whole new keyword, and I can't see them going for it when you can just say typeof (<MyClass>{}).property

@d180cf
Copy link

d180cf commented Sep 11, 2015

I'm curious, why is the new keyword needed at all? If it's removed from all the examples above, they will still make sense. For instance, if we have

interface Foo {
  a: number;
  b: string;
}

what's the difference between just Foo and typeon Foo and why would we need to write typeon Foo.a if Foo.a is just as good?

let x: Foo = null; // a common pattern
let y: Foo.a = 0; // this looks just as clear

It seems odd to require the typeon keyword only in the second declaration.

@rotemdan
Copy link
Author

@d180cf

My original proposed syntax was exactly that. The problem was that TypeScript has an edge feature to unify interface and namespace declarations that would make it ambiguous.

Also, to extend the idea to enable referencing class instance types:

class C {
  prop: number;
  static prop: string;
}

Using any one of let x: C.prop (which could also refer to the static property with the same name) or let x: typeof C.prop (which currently refers to the type of the static property) would be ambiguous so a new keyword (or at least an alternative syntax) was necessary. Since the semantics are very similar to typeof it felt reasonable to use something that would look similar. The best that I could find was typeon.

The concept of typeon is subtly different than typeof. typeof refers to the type of something "tangible", i.e. a run-time entity or object. typeon is about the type associated with the declaration itself. In the case of classes typeof C is the type of something tangible, i.e. the constructor itself, which is actually a function. typeon C is a "stretch" of the syntax and refers to the instance type associated with the declaration of C (the original idea was only about properties, but that seemed natural and useful to add).

In the case of interfaces, since interfaces are not tangible, it felt more semantically appropriate to use typeon than typeof. Also considering the case where a class implements an interface:

interface SomeInterface {
  a: number
}

class SomeClass implements SomeInterface {
  a: number;
}

It would be more consistent to use typeon with both i.e. typeon SomeInterface.a and typeon SomeClass.a.

@d180cf
Copy link

d180cf commented Sep 12, 2015

Now I think I get it. Wouldn't it be more correct then to write (typeon SomeClass).a since new A.B means new (A.B)?

Also, after I thought about your example in the other thread, I came to the conclusion, that the dot syntax isn't correct and it should be changed to (typeon SomeClass)#a. The typeon operator returns the internal representation of a type which, in case of Function, probably looks like this in tsc:

interface FunctionType extends Type {
  returnType: Type;
  arguments: Type[];
}

It seems obvious, that the type that describes object literals lists the members of an interface in member dictionary or an array of name-value pairs:

interface DictionaryType extends Type {
  members: { [name: string]: Type };
}

What we ultimately want to get is a way to access these internal properties defined someweher in tsc: in this thread we're discussing how to get access to the list of members and in the other thread I mentioned that it would be nice to access the return type. It seems obvious, that the return type and the members cannot be accessed at the same level, via the dot notation. However the following syntax would be free of ambiguities:

namespace ko {
  interface observable<T> {
    (): T; // getter
    (value: T): this; // setter
    subscribe(listener): { dispose(): void };
  }
}

type KN = ko.observable<number>;

var kn: KN;

type KNRT = (typeon KN).returnType; // the type of what kn() or kn(x) would return
type KNS = (typeon KN).members.subscribe; // (listener) => { dispose(): void }

But it's very likely that (typeon T).members.m will be the most commonly used pattern with typeon so it makes sense to introduce a shorter syntax for that. Such a syntax has already been invented to describe interfaces in JS:

Array.from; // the static Array.from(iterable) function
Array#forEach; // each array has the forEach method, but Array.forEach is undefined

This is why I think that (typeon Array)#forEach would be more correct. But then why not to allow an even shorter syntax: Array#from? It's also free of ambiguities. But if this syntax is allowed, then it sort of implies that typeon Array and just Array is about the same thing, which isn't true.

@rotemdan
Copy link
Author

@d180cf

In Typescript, a class has two essentially unrelated types associated with it:

  • The constructor type.
  • The instance type.

You're right that perhaps if the goal was to reference the class' instance type through a runtime entity which was not the instance itself, say, with its constructor, the concept of new would need to be taken in account and probably play a major role in the way the it was laid out.

However in the type realm, there's no meaning for new. A type cannot be 'newed'. That has no meaning for a purely declarative construct. The instance type is just a piece of compile-time metadata associated with the name SomeClass, and is not less or more "favored" than the constructor type. When trying to define something like typeon SomeClass, that doesn't necessarily carry a particular commitment to a runtime syntax or semantics as might be with typeof. It's essentially open-ended. In this case it was designed with the specific goal of referencing a property through a containing type, not a runtime entity. so there's need to require the programmer to use something (typeon SomeClass).prop. SomeClass.prop is just a path specifier, an address token for piece of compile-time type metadata.

The idea was to make it intuitive and familiar to programmers, and to try to 'match' the typeof syntax and semantics as closely as possible. I felt that the dot syntax (and also the this keyword, which is also [intentionally] reused with subtly different semantics) was preferable simply because otherwise nobody will use the feature (and it will probably not be accepted in the first place). It will seem too foreign and obscure with something like #, same with C++ style :: notation. Since it is very similar in concept and effect to typeof, there's no need to make it look more complicated than it actually is.

Using the . notation, on the other hand, does carry a form of commitment. In the mind of the developer, . means a tangible member that can be accessed at runtime. It would be confusing to overload it with a 'virtual' member like a return type, though I agree the semantics don't rule that out.

I considered alternatives to keywords like returnTypeOf and returnTypeOn, one of them of reusing the function call operator () as a signifier for the return type, e.g. , like typeof func() and typeon FuncType(). It has both advantages and disadvantages. I didn't think any one of these was good enough yet for a proposal, and looking to explore more options.

This was referenced Sep 15, 2015
@mhegazy mhegazy added the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Dec 10, 2015
@mhegazy
Copy link
Contributor

mhegazy commented Feb 22, 2016

closing in favor of #6606

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Duplicate An existing issue was already created Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

8 participants