-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Comments
Can the |
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: |
@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:
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
Further proposal: Allow |
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 |
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 class Process<T> {
options: {
priority: number;
type: T;
relatedProcesses: {
parentProcess: typeon this;
subProcessess: Array<typeon this>;
}
}
constructor(options: typeon this.options) {
..
}
} Using 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 |
@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 |
@yortus 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). |
@rotemdan fair enough. I guess the deeper the anonymous nesting goes, the less difference in effort there is between
and
|
@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:
You would write one of the following:
In addition, it could be used in more ways than 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 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; |
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 You could make your own You could modify Or you could write 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. |
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 [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 |
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 let x: Foo = null; // a common pattern
let y: Foo.a = 0; // this looks just as clear It seems odd to require the |
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 The concept of In the case of interfaces, since interfaces are not tangible, it felt more semantically appropriate to use interface SomeInterface {
a: number
}
class SomeClass implements SomeInterface {
a: number;
} It would be more consistent to use |
Now I think I get it. Wouldn't it be more correct then to write 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 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 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 |
In Typescript, a class has two essentially unrelated types associated with it:
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 However in the type realm, there's no meaning for The idea was to make it intuitive and familiar to programmers, and to try to 'match' the Using the I considered alternatives to keywords like |
closing in favor of #6606 |
Proposal: 'typeon' operator
The
typeon
operator 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
this
type referenceA 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:It would also be available in interfaces declared through
type
declarations 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 this
can only be used from instance positions. To reference static members, the name of the class would be used withtypeof
(note: no equivalenttypeof this
or a standalonethis
type 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 MyClass
only 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
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: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]
The text was updated successfully, but these errors were encountered: