Description
openedon Jan 25, 2016
Working Implementation for this Proposal
Try it out: npm install yortus-typescript-typeof
View the diff: here.
Problem Scenario
TypeScript's type inference covers most cases very well. However there remain some situations where there is no obvious way to reference an anonymous type, even though the compiler is able to infer it. Some examples:
I have a strongly-typed collection but the element type is anonymous/unknown, how can I reference the element type? (#3749)
// Mapping to a complex anonymous type. How to reference the element type?
var data = [1, 2, 3].map(v => ({ raw: v, square: v * v }));
function checkItem(item: typeof data[0] /* ERROR */) {...}
// A statically-typed dictionary. How to reference a property type?
var things = { 'thing-1': 'baz', 'thing-2': 42, ... };
type Thing2Type = typeof things['thing-2']; // ERROR
// A strongly-typed collection with special indexer syntax. How to reference the element type?
var nodes = document.getElementsByTagName('li');
type ItemType = typeof nodes.item(0); // ERROR
A function returns a local/anonymous/inaccessible type, how can I reference this return type? (#4233, #6179, #6239)
// A factory function that returns an instance of a local class
function myAPIFactory($http: HttpSvc, id: number) {
class MyAPI {
constructor(http) {...}
foo() {...}
bar() {...}
static id = id;
}
return new MyAPI($http);
}
function augmentAPI(api: MyAPI /* ERROR */) {...}
I have an interface with a complex anonymous shape, how can I refer to the types of its properties and sub-properties? (#4555, #4640)
// Declare an interface DRY-ly and without introducing extra type names
interface MyInterface {
prop1: {
big: {
complex: {
anonymous: { type: {} }
}
}
},
// prop2 shares some structure with prop1
prop2: typeof MyInterface.prop1.big.complex; // ERROR
}
Why do we need to Reference Anonymous/Inferred Types?
One example is declaring a function that takes an anonymous type as a parameter. We need to reference the type somehow in the parameter's type annotation, otherwise the parameter will have to be typed as any
.
Current Workarounds
Declare a dummy variable with an initializer that infers the desired type without evaluating the expression (this is important because we don't want runtime side-effects, just type inference). For example:
let dummyReturnVal = null && someFunction(0, ''); // NB: someFunction is never called!
let ReturnType = typeof dummyReturnVal; // Now we have a reference to the return type
This workaround has a few drawbacks:
- very clearly a kludge, unclear for readers
- identifier pollution (must introduce a variable like
dummyReturnValue
) - does't work in ambient contexts, because it requires an imperative statement
Proposed Solution
(NB: This solution was already suggested in #4233, but that issue is tagged 'Needs Proposal', and there are several other closely related issues, hence this separate issue.)
Allow typeof
's operand to be an arbitrary expression. This is already allowed for typeof expr
in a value position like if (typeof foo() === 'string')
. But this proposal also allows an arbitrary expression when typeof
is used in a type position as a type query, eg type ElemType = typeof list[0]
.
This proposal already aligns closely with the current wording of the spec:
Type queries are useful for capturing anonymous types that are generated by various constructs such as object literals, function declarations, and namespace declarations.
So this proposal is just extending that usefulness to the currently unserved situations like in the examples above.
Syntax and Semantics
The semantics are exactly as already stated in the spec 4.18.6:
The 'typeof' operator takes an operand of any type and produces a value of the String primitive type. In positions where a type is expected, 'typeof' can also be used in a type query (section 3.8.10) to produce the type of an expression.
The proposed difference relates to section 3.8.10 quoted below, where the struck-through text would be removed and the bold text added:
A type query consists of the keyword typeof followed by
an expression. The expression is restricted to a single identifier or a sequence of identifiers separated by periods. The expression is processed as an identifier expression (section 4.3) or property access expression (section 4.13)a unary expression, the widened type (section 3.12) of which becomes the result. Similar to other static typing constructs, type queries are erased from the generated JavaScript code and add no run-time overhead.
A point that must be emphasized (which I thought was also in the spec but can't find it) is that type queries do not evaluate their operand. That's true currently and would remain true for more complex expressions.
This proposal doesn't introduce any novel syntax, it just makes typeof
less restrictive in the types of expressions it can query.
Examples
// Mapping to a complex anonymous type. How to reference the element type?
var data = [1, 2, 3].map(v => ({ raw: v, square: v * v }));
function checkItem(item: typeof data[0]) {...} // OK: item type is {raw:number, square:number}
// A statically-typed dictionary. How to reference a property type?
var things = { 'thing-1': 'baz', 'thing-2': 42, ... };
type Thing2Type = typeof things['thing-2']; // OK: Thing2Type is number
// A strongly-typed collection with special indexer syntax. How to reference the element type?
var nodes = document.getElementsByTagName('li');
type ItemType = typeof nodes.item(0); // OK: ItemType is HTMLLIElement
// A factory function that returns an instance of a local class
function myAPIFactory($http: HttpSvc, id: number) {
class MyAPI {
constructor(http) {...}
foo() {...}
bar() {...}
static id = id;
}
return new MyAPI($http);
}
type MyAPI = typeof myAPIFactory(null, 0); // OK: MyAPI is myAPIFactory's return type
function augmentAPI(api: MyAPI) {...} // OK
// Declare an interface DRY-ly and without introducing extra type names
interface MyInterface {
prop1: {
big: {
complex: {
anonymous: { type: {} }
}
}
},
// prop2 shares some structure with prop1
prop2: typeof (<MyInterface>null).prop1.big.complex; // OK: prop2 type is {anonymous: {type: {}}}
}
Discussion of Pros/Cons
Against: Poor syntax aesthetics. Alternative syntaxes addressing individual cases have been suggested in #6179, #6239, #4555 and #4640.
For: Other syntaxes may look better for their specific cases, but they are all different from each other and each only solve one specific problem. This proposal solves the problems raised in all those issues, and the developer doesn't need to learn any new syntax(es).
Against: An expression in a type position is confusing.
For: TypeScript already overloads typeof
with two meanings, as a type query it already accepts an expression in a type position and gets its type without evaluating it. This just relaxes the constraints on what that expression can be so that it can solve the problems raised in this issue.
Against: This could be abused to write huge long multi-line type queries.
For: There's no good reason to do that in a type query, but there are good reasons to allow more complex expressions. This is basically Martin Fowler's enabling vs directing.
Design Impact, Questions, and Further Work
Compatibility
This is a purely backward-compatible change. All existing code is unaffected. Using the additional capabilities of typeof
is opt-in.
Performance
Looking at the diff you can see the changes are very minor. The compiler already knows the types being queried, this just surfaces them to the developer. I would expect negligable performance impact, but I don't know how to test this.
Tooling
I have set up VS Code to use a version of TypeScript with this proposal implemented as its language service, and all the syntax highlighting and intellisense is flawless as far as I have tested it.
Complex expressions may occur in .d.ts
files
typeof
's operand could be any expression, including an IIFE, or a class expression complete with method bodies, etc. I can't think of any reason to do that, it's just no longer an error, even inside a.d.ts
file (typeof
can be used - and is useful - in ambient contexts). So a consequence of this proposal is that "statements cannot appear in ambient contexts" is no longer strictly true.
Recursive types are handled robustly
The compiler seems to already have all the logic in place needed to deal with things like this:
function foo<X,Y>(x: X, y: Y) {
var result: typeof foo(x, y); // ERROR: 'result' is referenced in its own type annotation
return result;
}
Can query the return type of an overloaded function
It is not ambiguous; it picks the overload that matches the query's expression:
declare function foo(a: boolean): string;
declare function foo(a: number): any[];
type P = typeof foo(0); // P is any[]
type Q = typeof foo(true); // Q is string