-
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: Get the type of any expression with typeof #6606
Comments
For anyone wanting a quick way to play with this in VS Code with intellisense etc, here is a playground repo. |
I think using types as argument instead of values is a more valid syntax. type P = typeof foo(number); // P is any[]
type Q = typeof foo(boolean); // Q is string It is more clear that the function is not being called, because you provide types and not values as arguments. The other point, is it is less ambiguous. Some people will use |
@tinganho I've been giving your idea some thought and I see some things I prefer about this proposal, and other things I prefer about your suggestion. Your suggestion is good for the reasons you gave (simpler clearer syntax, less ambiguous, looks less like a function call). The thing I prefer about my proposal is that it doesn't introduce any novel syntax, so does not add any complications to the parser/checker, and it also supports more complex scenarios where you don't have simple type names for the arguments. I was thinking, what if there was a very shorthand way of writing something like your With this change (which I've implemented in the playground repo), you can write something much closer to your suggested syntax, but it is still parsed as an ordinary expression: type P = typeof foo(as number); // P is any[]
type Q = typeof foo(as boolean); // Q is string
let prop2: typeof (as MyInterface).prop1.big.complex; // prop2 type is {anonymous: {type: {}}} This was just a quick experiment. An equivalent syntax could be (but I haven't implemented this): type P = typeof foo(<number>); // P is any[]
type Q = typeof foo(<boolean>); // Q is string
let prop2: typeof (<MyInterface>).prop1.big.complex; // prop2 type is {anonymous: {type: {}}} The second syntax might be called a nullary type assertion, with the expression |
@yortus, we spent some time last week talking about this proposal. sorry for not posting earlier. the consensus was 1. we have a problem of not being able to refer to some types, e.g. return of a function or instance type of a class expression. and 2. adding expressions in a type position is not something we are comfortable with. @tinganho's proposal was one that we talked about as well. i think it is more palatable, though would probably be more complicated to implement. Adding a new unary operator or using cast syntax is not really elegant as just using the type names. |
Discussed for quite a while at the slog today. "Accepting PRs" here is "Accepting PRs assuming the implementation doesn't turn out to be too crazy" @tinganho 's proposal looks pretty good (relative to the other options, at least) and we'd like to see a tentative PR that implements this. The tricky thing is that we don't want to have a completely separate codepath for resolving the return type of Basic plan of attack would be:
|
@mhegazy, @RyanCavanaugh not sure how many corner cases the team discussed, so can I bring up a few here for clarification? I've listed a bunch of examples below and commented each one with what I think should be the result of the Indexer notationvar data = [1, 2, 3];
const LOBOUND = 0;
type Elem1 = typeof data[0]; // number
type Elem2 = typeof data[999999]; // number or ERROR?
type Elem3 = typeof data[1+2]; // ERROR or number?
type Elem4 = typeof data[LOBOUND]; // ERROR or number?
var tuple: [number, string] = [123, 'abc'];
type Elem4 = typeof tuple[0]; // number or number|string?
type Elem5 = typeof tuple[1]; // string or number|string?
type Elem6 = typeof tuple[999999]; // number|string or ERROR?
const ABC: 'a-b-c' = 'a-b-c';
let dict = { 'a-b-c': 123, 'd-e-f': true };
type Prop1 = typeof dict['a-b-c']; // number
type Prop2 = typeof dict['d-e-f']; // boolean
type Prop3 = typeof dict[ABC]; // ERROR or number or any? Function return type notation// A simple function
declare function f1(n: number): string[];
type Ret1 = typeof f1(number); // string[]
type Ret2 = typeof f1(0); // ERROR or string[]?
// An asynchronous function that either accepts a callback or returns a Promise
declare function f2(n: number): Promise<string[]>;
declare function f2(n: number, cb: (err?: any, result?: string[]) => void): void;
type Ret3 = typeof f2(number); // Promise<string[]>
type Ret4 = typeof f2(number, any); // void
type Ret5 = typeof f2(number, Function); // ERROR: Function not assignable to callback
type Ret6 = typeof f2(number, (err: any, result: string[]) => void); // void
type Ret7 = typeof f2(number, (...args) => any); // void
// A special function-like object
interface Receiver {
(data: string[]): void;
transmogrify(): number[];
}
declare function f3(n: number, receiver: Receiver): Promise<void>;
declare function f3(n: number, callback: (err?: any, result?: string[]) => void): void;
type Ret8 = typeof f3(number, Receiver); // Promise<void>
type Ret9 = typeof f3(number, any); // ambiguous? or picks first overload?
type Ret10 = typeof f3(number, Function); // ERROR
type Ret11 = typeof f3(number, (...args) => any); // void since not assignable to Receiver
// A function with parameter destructuring
interface CartesianCoordinate {/***/}
interface PolarCoordinate {/***/}
declare function f4({ x: number, y: number }): CartesianCoordinate;
declare function f4({ r: number, t: number }): PolarCoordinate;
type Ret12 = typeof f4(any); // ambiguous? or picks first overload?
type Ret13 = typeof f4({x;y}); // CartesianCoordinate
type Ret14 = typeof f4({r;t}); // PolarCoordinate
type Ret15 = typeof f4({x;r;t;y}); // ambiguous? or picks first overload?
// Type-ception: is there anything wrong with typeof-in-typeof?
declare function f5(n: number, receiver: Receiver): Promise<void>;
declare function f5(n: number, callback: (err?: any, result?: string[]) => void): void;
function myCallback(err, result) {/***/}
var myReceiver: Receiver;
type Ret16 = typeof f5(number, typeof myReceiver); // Promise<void>
type Ret17 = typeof f5(number, typeof myCallback); // void Extracting part of a class/interface typeThat is, the I take it from above comments that this is out of scope and will not be supported. Is that correct? |
Indexer notationvar data = [1, 2, 3];
const LOBOUND = 0;
type Elem1 = typeof data[0]; // number
type Elem2 = typeof data[999999]; // number
type Elem3 = typeof data[1+2]; // ERROR, only literals allowed here
type Elem4 = typeof data[LOBOUND]; // number when const resolution is done, otherwise any
var tuple: [number, string] = [123, 'abc'];
type Elem4 = typeof tuple[0]; // number
type Elem5 = typeof tuple[1]; // string
type Elem6 = typeof tuple[999999]; // number|string
const ABC: 'a-b-c' = 'a-b-c';
let dict = { 'a-b-c': 123, 'd-e-f': true };
type Prop1 = typeof dict['a-b-c']; // number
type Prop2 = typeof dict['d-e-f']; // boolean
type Prop3 = typeof dict[ABC]; // number when const resolution work is done, otherwise any Function return type notation// A simple function
declare function f1(n: number): string[];
type Ret1 = typeof f1(number); // string[]
type Ret2 = typeof f1(0); // error, 0 is not a type
// An asynchronous function that either accepts a callback or returns a Promise
declare function f2(n: number): Promise<string[]>;
declare function f2(n: number, cb: (err?: any, result?: string[]) => void): void;
type Ret3 = typeof f2(number); // Promise<string[]>
type Ret4 = typeof f2(number, any); // void
type Ret5 = typeof f2(number, Function); // ERROR: Function not assignable to callback
type Ret6 = typeof f2(number, (err: any, result: string[]) => void); // void
type Ret7 = typeof f2(number, (...args) => any); // void
// A special function-like object
interface Receiver {
(data: string[]): void;
transmogrify(): number[];
}
declare function f3(n: number, receiver: Receiver): Promise<void>;
declare function f3(n: number, callback: (err?: any, result?: string[]) => void): void;
type Ret8 = typeof f3(number, Receiver); // Promise<void>
type Ret9 = typeof f3(number, any); // picks first overload
type Ret10 = typeof f3(number, Function); // ERROR
type Ret11 = typeof f3(number, (...args) => any); // void since not assignable to Receiver
// A function with parameter destructuring
interface CartesianCoordinate {/***/}
interface PolarCoordinate {/***/}
declare function f4({ x: number, y: number }): CartesianCoordinate;
declare function f4({ r: number, t: number }): PolarCoordinate;
type Ret12 = typeof f4(any); // picks first overload
type Ret13 = typeof f4({x;y}); // CartesianCoordinate
type Ret14 = typeof f4({r;t}); // PolarCoordinate
type Ret15 = typeof f4({x;r;t;y}); // picks first overload
// Type-ception: is there anything wrong with typeof-in-typeof?
declare function f5(n: number, receiver: Receiver): Promise<void>;
declare function f5(n: number, callback: (err?: any, result?: string[]) => void): void;
function myCallback(err, result) {/***/}
var myReceiver: Receiver;
type Ret16 = typeof f5(number, typeof myReceiver); // Promise<void>
type Ret17 = typeof f5(number, typeof myCallback); // void |
I'm curious what happens here: const number = "number";
type Ret3 = typeof f2(number); // What happens here? |
@saschanaz good question. A similar situation: class MyClass { foo; bar; }
declare function f(inst: MyClass): number;
type Ret = typeof f(MyClass); // number (presumably) In this case it makes sense in Would the same logic apply to a name that referred to both a type and a const value? In your example that would mean the type |
Right, we'd resolve this under the usual semantics of a type expression (as if you had written |
So then @saschanaz's example unambiguously refers to const number = "number";
type Ret3 = typeof f2(number); // Promise<string[]> |
@RyanCavanaugh can you confirm that the third group of use-cases is out of scope? e.g. from the OP: // 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: {}}}
} This use-case (with whatever syntax) will not be supported at this time, is that right? |
I thint this should be coverd by allowing prop2: typeof this.prop1.big.complex; |
I think the type Ret3 = typeof f2(typeof number); // typeof number is string so error here ... while this would block |
@mhegazy that's a great idea re class MyClass {
prop1: {
big: {
complex: {
anonymous: { type: {} }
}
}
};
prop2: typeof this.prop1.big.complex; // prop2 type is {anonymous: {type: {}}}
}
interface MyInterface {
prop1: {
big: {
complex: {
anonymous: { type: {} }
}
}
};
prop2: typeof this.prop1.big.complex; // prop2 type is any
} Is inferring |
I want to make two points about #6179 and Angular. // A factory function that returns an instance of a local class
function myAPIFactory($http: HttpSvc, id: number) {
class MyAPI {
constructor(token: string) {...}
foo() {...}
bar() {...}
static id = id;
}
return MyAPI;
}
type MyAPIConstructor = typeof myAPIFactory(null, 0); // OK: MyAPI is myAPIFactory's return type
function augmentAPI(api: MyAPIConstructor) {...} // OK
|
I agree that it is possible to do things using conditional types. I think that it would not bring much additional complexity in the compiler if we would rewrite export type Avatar = User extends { avatar: infer T } ? T : never; as export type Avatar = User.avatar; to improve readability. Full exampleSuppose we load and transform some data, and end up with a function findUser like this export function findUser() {
return {
username: 'johndoe',
avatar: {
lg: '1.jpg',
s: '2.jpg'
},
repos: [
{
name: 'ts-demo',
stats: {
stars: 42,
forks: 4
},
pull_requests: [
{ date: '2019-08-19', tags: ['bug', 'agreed-to-cla'] },
{ date: '2019-08-10', tags: ['bug', 'includes-tests'] },
{ date: '2019-08-07', tags: ['feature'] }
]
}
]
};
} Thanks to the inference from mapped types, we can extract the type from the function like so: export type User = ReturnType<typeof findUser>;
export type Avatar = User extends { avatar: infer T } ? T : never; Suggestion: this should evaluate to the same thing export type Avatar = User.avatar; Additionally, we could even assert that More examples export type Repositories = User extends { repos: infer T } ? T : never;
export type Repository = User extends { repos: (infer T)[] } ? T : never;
export type RepositoryStats = Repository extends { stats: infer T } ? T : never;
export type PullRequests = Repository extends { pull_requests: (infer T)[] } ? T : never;
export type PullRequest = Repository extends { pull_requests: (infer T)[] } ? T : never;
export type Tags = PullRequest extends { tags: infer T } ? T : never;
export type Tag = PullRequest extends { tags: (infer T)[] } ? T : never; export type Repositories = User.repos;
export type Repository = User.repos[];
export type RepositoryStats = User.repos[].stats;
export type PullRequests = User.repos[].pull_requests;
export type PullRequest = User.repos[].pull_requests[];
export type Tags = User.repos[].pull_requests[].tags;
export type Tag = User.repos[].pull_requests[].tags[]; When mapping a nested property in one go, it is not very clear what is happening export type Tag2 = User extends { repos: { pull_requests: { tags: (infer T)[] }[] }[] } ? T : never; This would clearify it a lot export type Tag = User.repos[].pull_requests[].tags[]; Corner caseexport class Hello {
static world = 'world';
world = 42;
} export type ThisWillBeANumber = Hello extends { world: infer T } ? T : never;
export type ThisWillBeANumber = Hello.world; export type ThisWillBeAString = (typeof Hello) extends { world: infer T } ? T : never;
export type ThisWillBeAString = (typeof Hello).world; |
@lukaselmer It seems like you just want export type Avatar = User["avatar"]; which works today |
That's exactly what I was looking for. I was searching for it in the documentation, but didn't find it. Thank you! |
Is this part of the handbook, or is there any official documentation on how this works? Im pretty familiar myself on how to use it, but when I try to direct people to documentation, all I can find is typeof guards, which is really completely different |
So, I have noticed that this proposal bounced around from 2015 and one of the original goals was to somehow get the type of a single property of an interface.
am I correct to assume that this is still not possible 5 years later? |
@MFry I think you're looking for this syntax: |
Do we know if there is a solution for this yet? I'm trying to get something like this: declare function something<A, B>(): void;
type Payload = string;
const hello = something<{}, Payload>();
declare function doThing<T extends ReturnType<typeof something>>(arg: T): { payload: unknown };
doThing(hello).payload === 123; // this should validate to a string aka type Payload |
Hi @maraisr I'm not 100% sure what you are trying to achieve. In your example Maybe something like below is what you want? declare function something<ReturnType>(): ReturnType;
type Payload = string;
const hello = () => something<Payload>();
declare function doThing<F extends () => any>(f: F): { payload: ReturnType<F> };
doThing(hello).payload === 'a string'; |
Ah yeah - so sorry about that. Thank you for the prompt response!! 💯 @acutmore The something like: declare function something<A, B>(a: MyComplexGeneric<A>, b: B[]): { somethingA: number, somethingB: number };
// Those 2 generics influence the return object so they do get used as such. And the 2 arguments are roughly that. Its an object and an array, more-or-less. My See that So I can't simply just get the ReturnType of a function - i need to somehow suck out the generic of a created function. If you feel this query is beyond the scope of this issue, ill continue my journey on StackOverflow! |
@maraisr thanks for the extra info. If you want to This is one way that this can be done: /** Create a type that can store some extra type information **/
interface SomethingResult<T> {
__$$__: T;
somethingA: number;
somethingB: number;
}
declare function something<A, B>(): SomethingResult<B>;
type Payload = string;
const hello = something<{}, Payload>();
declare function doThing<Result extends SomethingResult<any>>(arg: Result): { payload: Result['__$$__'] };
doThing(hello).payload === 1123; // error because `payload` is of type string |
|
@RyanCavanaugh Why is this closed? Conditional Types don't solve this and many other use cases, and if this gets merged it would make so many things possible. I'm working on a function that can turn any method call into a "point-free" version (example: If I could "call" the functions to get the type ( Is the complexity very high to be able to |
@nythrox we don't feel that the syntactic confusion that this could lead to is outweighed by the cases where you need it to get to some type. The specific case of resolving a call expression is tracked elsewhere; the proposal in the OP of "allow any expression whatsoever" isn't something we think would be a good fit for the language. |
@RyanCavanaugh oh okay, I understand. Thanks for the response, do you know what issues are tracking resolving a function call? |
I have searched around a bit and have not found an issue for a function call utility type; the only reference to one I found was in #20352, which just linked back to this issue.
@RyanCavanaugh Mind linking to elsewhere? 🙂 |
@acutmore That is somewhat along the lines of what I was looking for, though I was specifically talking about a flow-esque |
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)
A function returns a local/anonymous/inaccessible type, how can I reference this return type? (#4233, #6179, #6239)
I have an interface with a complex anonymous shape, how can I refer to the types of its properties and sub-properties? (#4555, #4640)
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:
This workaround has a few drawbacks:
dummyReturnValue
)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 fortypeof expr
in a value position likeif (typeof foo() === 'string')
. But this proposal also allows an arbitrary expression whentypeof
is used in a type position as a type query, egtype ElemType = typeof list[0]
.This proposal already aligns closely with the current wording of the spec:
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 proposed difference relates to section 3.8.10 quoted below, where the struck-through text would be removed and the bold text added:
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
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
filestypeof
'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:
Can query the return type of an overloaded function
It is not ambiguous; it picks the overload that matches the query's expression:
The text was updated successfully, but these errors were encountered: