Skip to content

Proposal: Get the type of any expression with typeof #6606

Closed

Description

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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Assignees

No one assigned

    Labels

    SuggestionAn idea for TypeScriptToo ComplexAn issue which adding support for may be too complex for the value it adds

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions