Repository navigation
Proposal: 'typeon' operator #4640
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScript
on Sep 4, 2015 Can the
typeofoperator not be extended to support these cases?DanielRosenwasser commented
on Sep 4, 2015 MemberMore actionsCan the
typeofoperator 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;
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
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.Daniel Rosenwasser (@DanielRosenwasser) , Dan Quirk (@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
typeofaccepted 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 Rotem Dan (@rotemdan) describes. sotypeof func(<number> 10).acould be writtentypeof func(<number>).aProposal: 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
decltypeand it maps an expression to a type without evaluating the expression. Given the limitations currently placed ontypeof, it could be extended to work just likedecltypewithout a breaking change. It would become far more useful.Troy Gerwien (@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
returnTypeOffor example), so I think these are mostly unrelated proposals.Rotem Dan (@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
typeonin your example (inside the options interface) aren't needed at all.Troy Gerwien (@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).
Rotem Dan (@rotemdan) fair enough. I guess the deeper the anonymous nesting goes, the less difference in effort there is between
typeon Process.state.processorUsageand
typeof new Process().state.processorUsageRotem Dan (@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.prop3with no loss of function. Or alternativelytypeof 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
undefinedto the desired type (or writing(new Type())as Troy Gerwien (@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;
// 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, sayKnex. UnfortunatelyQueryBuilderonly appears as a return value fromKnex'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.tsto expose its privateQueryBuilderand 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.
typeofcould easily express this and more.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
typeofis.[I continued my comment at #4233 because it felt a bit off-topic here.]
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>{}).propertyI'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
Fooandtypeon Fooand why would we need to writetypeon Foo.aifFoo.ais 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
typeonkeyword only in the second declaration.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) orlet 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 totypeofit felt reasonable to use something that would look similar. The best that I could find wastypeon.The concept of
typeonis subtly different thantypeof.typeofrefers to the type of something "tangible", i.e. a run-time entity or object.typeonis about the type associated with the declaration itself. In the case of classestypeof Cis the type of something tangible, i.e. the constructor itself, which is actually a function.typeon Cis a "stretch" of the syntax and refers to the instance type associated with the declaration ofC(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
typeonthantypeof. 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
typeonwith both i.e.typeon SomeInterface.aandtypeon SomeClass.a.Now I think I get it. Wouldn't it be more correct then to write
(typeon SomeClass).asincenew A.Bmeansnew (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. Thetypeonoperator returns the internal representation of a type which, in case ofFunction, 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.mwill be the most commonly used pattern withtypeonso 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)#forEachwould 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 thattypeon Arrayand justArrayis about the same thing, which isn't true.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
newwould 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 nameSomeClass, and is not less or more "favored" than the constructor type. When trying to define something liketypeon SomeClass, that doesn't necessarily carry a particular commitment to a runtime syntax or semantics as might be withtypeof. 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.propis 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
typeofsyntax and semantics as closely as possible. I felt that the dot syntax (and also thethiskeyword, 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 totypeof, 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
returnTypeOfandreturnTypeOn, one of them of reusing the function call operator()as a signifier for the return type, e.g. , liketypeof func()andtypeon 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.- addedNeeds ProposalThis issue needs a plan that clarifies the finer details of how it could be implemented.This issue needs a plan that clarifies the finer details of how it could be implemented.
on Dec 10, 2015 closing in favor of #6606
- addedDuplicateAn existing issue was already createdAn existing issue was already created
on Feb 22, 2016 - locked and limited conversation to collaborators
on Jun 19, 2018
Proposal: 'typeon' operator
The
typeonoperator references the type associated with an interface or class property, but in contrast totypeof, 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:This would work with any property, including ones contained in anonymous and nested interfaces:
Generic interfaces:
It would be possible to reference the property types at any scope, including within the referenced declaration itself:
The
thistype referenceA special
thistype reference could be supported for this very purpose.thiswould be scoped to the closest containing interface, and that would also apply to anonymous ones:It would also be available in interfaces declared through
typedeclarations and for purely anonymous ones: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 forsuper):In a class declaration,
typon thiscan only be used from instance positions. To reference static members, the name of the class would be used withtypeof(note: no equivalenttypeof thisor a standalonethistype is included in this proposal).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:
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:
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:
In the conventional style of coding, 4 auxiliary interfaces or type aliases would need to be declared to achieve this:
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 MyClassonly gives the type of the constructor):Equivalent reduction to named type aliases
This can be internally implemented in the compiler like this:
Source:
Reduction:
Example with generic interfaces:
Source:
Reduction (this uses the new generic type alias declarations introduced in 1.6):
Possible issues and their solutions
Detect and error on circular references:
This would happen automatically through the reduction described above. The error currently reported through
typeis "Error: type 'TypeOn_EndlessLoop_y' circularly references itself".Current workarounds
It is currently possible to partially emulate this using
typeofwith a "dummy" instance of the type:And even:
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(orsuper) thus cannot be used within anonymous types.References to generic parameters cannot be emulated:
[Originally described at #4555]