Skip to content
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

Closed
yortus opened this issue Jan 25, 2016 · 157 comments
Closed

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

yortus opened this issue Jan 25, 2016 · 157 comments
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds

Comments

@yortus
Copy link
Contributor

yortus commented 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
@yortus
Copy link
Contributor Author

yortus commented Jan 26, 2016

For anyone wanting a quick way to play with this in VS Code with intellisense etc, here is a playground repo.

@tinganho
Copy link
Contributor

type P = typeof foo(0); // P is any[]
type Q = typeof foo(true); // Q is string

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 typeof foo(false), whereas some people will use typeof foo(true). If you have types as arguments people can only write typeof foo(boolean).

@Artazor
Copy link
Contributor

Artazor commented Jan 30, 2016

@tinganho exactly!
Though we still can write typeof foo("abc") with #5185
here "abc" is the singleton string type

@yortus
Copy link
Contributor Author

yortus commented Jan 31, 2016

@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 foo(number) syntax but using the existing expression parsing mechanics? So as an experiment I've introduced a new expression: unary as. You can just write as T and that's shorthand for (null as T). You are basically saying, 'I don't care about the value but I want the expression to have type X'.

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 <T> being shorthand for (<T> null).

@mhegazy
Copy link
Contributor

mhegazy commented Feb 1, 2016

@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.

@RyanCavanaugh RyanCavanaugh added Help Wanted You can do this Effort: Difficult Good luck. and removed In Discussion Not yet reached consensus labels Feb 1, 2016
@RyanCavanaugh RyanCavanaugh added this to the Community milestone Feb 1, 2016
@RyanCavanaugh
Copy link
Member

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 f(number) vs f(0), but the overload resolution algorithm is totally baked-in with the assumption that it's working with a set of expressions rather than a set of types. But we think with a little trickery this should be straightforward.

Basic plan of attack would be:

  • Expand the grammar when parsing typeof to allow things that look like function calls, property access, and indexed property access
  • When parsing the arguments to a function call in a type query (let's call them psuedocalls / psuedoarguments), use the parseType function. This is going to create a TypeNode, but set a flag on the node that indicates that it was a type parsed in the context of a type query
  • In the checker, checkExpression checks for this flag and calls getTypeFromTypeNode instead of normal expression processing

@yortus
Copy link
Contributor Author

yortus commented Feb 2, 2016

@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 typeof operation, with question marks on questionable cases.


Indexer notation
var 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 type

That is, the typeof (<MyInterface> null).prop1.big.complex; examples above.

I take it from above comments that this is out of scope and will not be supported. Is that correct?

@RyanCavanaugh
Copy link
Member

Indexer notation
var 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

@saschanaz
Copy link
Contributor

I'm curious what happens here:

const number = "number";
type Ret3 = typeof f2(number); // What happens here?

@yortus
Copy link
Contributor Author

yortus commented Feb 2, 2016

@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 typeof f(MyClass) for the MyClass type to be considered before the MyClass value (ie the constructor function). The former leads to Ret = number, the latter would lead to something like error: MyClass is not a type.

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 number would always take precedence over the const value number. Any thoughts @RyanCavanaugh?

@RyanCavanaugh
Copy link
Member

Right, we'd resolve this under the usual semantics of a type expression (as if you had written var x: [whatever]). So you could have typeof f(MyClass) referring to invoking f with the instance side, and typeof f(typeof MyClass) referring to invoking f with the constructor function.

@yortus
Copy link
Contributor Author

yortus commented Feb 2, 2016

So then @saschanaz's example unambiguously refers to number as a type, not as the const value, right?

const number = "number";
type Ret3 = typeof f2(number); // Promise<string[]>

@yortus
Copy link
Contributor Author

yortus commented Feb 2, 2016

@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?

@mhegazy
Copy link
Contributor

mhegazy commented Feb 2, 2016

I thint this should be coverd by allowing this expressions.

   prop2: typeof this.prop1.big.complex;

@saschanaz
Copy link
Contributor

I think the const thing should be resolved by another typeof.

type Ret3 = typeof f2(typeof number); // typeof number is string so error here

... while this would block typeof data[LOBOUND].

@yortus
Copy link
Contributor Author

yortus commented Feb 2, 2016

@mhegazy that's a great idea re typeof this. I just realised this already works in the forked implementation I made for this proposal. Well, it works for classes. For interfaces there is no error, but the this type is always inferred as any. Current output from the forked impl:

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 this inside interface declarations a possibility or would this feature remain limited to classes?

@thorn0
Copy link

thorn0 commented Feb 26, 2016

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
  1. The number of parameters can be big. Let's say 15 parameters. At the same time there is no overloads, and it's only the overloads that the parameters in typeof are needed for. So for this case, can we probably think of a syntax like following?

    type MyAPI = typeof myAPIFactory(...);
  2. The factory function isn't usually assigned to an own global variable. A function expression is used:

    angular.module('app').factory('MyAPI', function($http: HttpSvc, id: number) { /*...*/ });

    That's what it usually looks like. As can be seen, typeof can't be used here at all.

@lukaselmer
Copy link
Contributor

Conditional types now make this largely irrelevant since you can write ReturnTypeOf<T> along with specific other aliases if you want to validate a particular set of arguments. They can't do overload resolution but we don't think this feature is worth the complexity just for that use case.

@RyanCavanaugh @mhegazy

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 User.avatar to User extends { avatar: infer T } ? T : never? So for example we could write

export type Avatar = User extends { avatar: infer T } ? T : never;

as

export type Avatar = User.avatar;

to improve readability.

Full example

Suppose 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 User.avatar must not be of type never.

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 case

export 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;

@RyanCavanaugh
Copy link
Member

@lukaselmer It seems like you just want

export type Avatar = User["avatar"];

which works today

@lukaselmer
Copy link
Contributor

@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!

@yonilerner
Copy link

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

@MFry
Copy link

MFry commented May 6, 2020

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.

interface a {
 foo: bar;
 /* more types */
}

const example = (fooNeeded: [magic] a.foo ) => {};

am I correct to assume that this is still not possible 5 years later?

@acutmore
Copy link
Contributor

acutmore commented May 6, 2020

@MFry I think you're looking for this syntax: a['foo']

@maraisr
Copy link
Member

maraisr commented May 14, 2020

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

https://www.typescriptlang.org/play/index.html?ts=4.0.0-dev.20200512#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwGccBbEDACy1QHMAeAQQBp4AhAPgAoBKALngDccWYAG4AUGIwBPAA4IAClCkQcUYPAC8hDDCrVxYsHgIZ45EBBWbCJMpRq0A3gF9mi5auCcuB0JFgIKOjYePDAOAAq9nQR8CAAHhggqMAE8ABKZMgwqBGyILTScjiINqQUemycsNR8EbzwjvAySipqfGgA1qg4AO74zr6R0RzmljhcAHQtHmqaGloAjABMAMwi8AD0m-AVaQTkOMgQ6vxQEMJQSbs48FDaujR3nfdFCq2eYkA

@acutmore
Copy link
Contributor

Hi @maraisr I'm not 100% sure what you are trying to achieve. In your example something takes two types but does not use them, and hello is the return value of something which will always be void; So doThing never sees the type string at any point.

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';

@maraisr
Copy link
Member

maraisr commented May 14, 2020

Ah yeah - so sorry about that. Thank you for the prompt response!! 💯 @acutmore

The void was just to indicate that the returntype of that function is irrelevant. Those 2 types get forwarded onto other generic types, which ultimately get used in arguments.

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 doThing function doesn't really care what the first (A) generic is, but it does care what the second (B) on is.

See that something in my own usecase does a sideeffect which get read by the doThing.

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!

@acutmore
Copy link
Contributor

acutmore commented May 14, 2020

@maraisr thanks for the extra info.

If you want to doThing to be able to get the original B type from something then it needs to be passed to hello in someway. TypeScript is only looking at hello and without some help it won't know that it is the return type of something.

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

@bobrosoft
Copy link

interface User {
  avatar: string;
}

interface UserData {
  someAvatar: User['avatar'];
}

@nythrox
Copy link

nythrox commented Sep 21, 2020

@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: [].map(() => n > 5) turns into map(() => n > 5)([]) and the only thing missing is that Conditional Types and infer can't detect generics, so in generic functions some types will come out as unknown.

If I could "call" the functions to get the type ( typeof myFunc(() => Either<string,number>)) it would be possible to have this functionality (that is currently impossible), and make many other things much easier to do (HKTs, etc...)

Is the complexity very high to be able to $Call a function (like in flow)? I feel like typescript already does it automatically.

@RyanCavanaugh
Copy link
Member

@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.

@nythrox
Copy link

nythrox commented Sep 21, 2020

@RyanCavanaugh oh okay, I understand. Thanks for the response, do you know what issues are tracking resolving a function call?

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 22, 2020

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.

The specific case of resolving a call expression is tracked elsewhere

@RyanCavanaugh Mind linking to elsewhere? 🙂

@acutmore
Copy link
Contributor

@tjjfvi #37181 is more specically about resolving a function based on its inputs. Might be what you are looking for.

@tjjfvi
Copy link
Contributor

tjjfvi commented Oct 22, 2020

@acutmore That is somewhat along the lines of what I was looking for, though I was specifically talking about a flow-esque $Call utility, or other syntax to be able to implement such. The approach suggested there is strange, but thanks for the link.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Suggestion An idea for TypeScript Too Complex An issue which adding support for may be too complex for the value it adds
Projects
None yet
Development

No branches or pull requests