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

Suggestion: Using typeof with an expression #4233

Closed
Andael opened this issue Aug 8, 2015 · 21 comments
Closed

Suggestion: Using typeof with an expression #4233

Andael opened this issue Aug 8, 2015 · 21 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@Andael
Copy link

Andael commented Aug 8, 2015

It would be helpful if the typeof keyword were changed to support more complex expressions, such as function calls.

Example

Given the following AngularJS service definition:

module myApp {
  function myApiFactory($http: HttpSvc) {
    return {
      getSetting: (key: string) => $http.get(etc),
    };
  }
  angular.service("myApi", myApiFactory);
}

It would be good if the following statement were possible:

export type MyApi = typeof myApiFactory();

Explanation

Since the function is a factory rather than the actual service, this type alias would other components to have a strongly-typed reference to the service. Though this example relates to AngularJS' service factory and dependency injection, this particular change would not be solely for AngularJS support.

Existing solutions

One option to resolve this issue is to explicitly define an interface for the service. However, this solution would require repeating the signature for all of the service's methods.

A workaround exists that relies on JavaScript's scoping rules to satisfy TypeScript's typeof keyword. However, it significantly reduces readability and clarity. An example:

if (false)
  var obj = myApiFactory(null);
export type MyApi = typeof obj;
@lazdmx
Copy link

lazdmx commented Aug 8, 2015

👍 Seems related to #3749 (comment) as means to get collection element type through expression.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 8, 2015

I find Using an expression with potential sides effects in a type position really confusing.

@mhegazy
Copy link
Contributor

mhegazy commented Aug 8, 2015

Also can you elaborate on how the API and the type would be used outside the module?

@yortus
Copy link
Contributor

yortus commented Aug 8, 2015

I have found myself wishing this were possible sometimes - usually when using type definitions from DefinitelyTyped. For example, knex.d.ts internally defines a QueryBuilder interface but does not export it, even though most knex methods return instances of this type. That makes it a pain to, say, write a function that takes a QueryBuilder as an argument.

A workaround is to create a 'dummy' variable with the desired type using an expression that is not necessarily evaluated, and then using typeof on that. In the knex case, the following works:

import Knex = require('knex');
var dummyQueryBuilder = false ? Knex(<any>{})() : null;
type QueryBuilder = typeof dummyQueryBuilder;

The proposal outlined by @ander-nz is far cleaner than this, although I agree with @mhegazy that putting expressions in type positions may be confusing.

The best approach in this example is to fix knex.d.ts and sumbit a PR to DefinitelyTyped, but that's a 'big fix' approach that may not always be possible, and for which typeof expr could provide a nice short-term fix in the meantime.

@yortus
Copy link
Contributor

yortus commented Aug 8, 2015

This would be equivalent of C++'s decltype(expr), which is used in a type position but does not evaluate expr. It's a very useful facility for writing reuseable code that propagates types without foreknowledge of their names (or shapes).

@Andael
Copy link
Author

Andael commented Aug 9, 2015

@mhegazy I agree that the syntax could be confusing, as per below:

var a = typeof myApiFunc(); // actually invokes myApiFunc, and then sets a to "object".
type A = typeof myApiFunc(); // does not invoke it, only looks at its return type.

A possible alternate syntax is using <``> brackets, since these are already used by TypeScript. For example:

type MyApi = typeof<myApiFunc()>;

This could simplify the syntax, and would look out of place enough to remind the reader that the expression is a TypeScript feature. Does this look clearer?

@Andael
Copy link
Author

Andael commented Aug 9, 2015

@mhegazy And to answer your question about how the type would be used outside the module. That example is designed for AngularJS, which uses dependency injection, such as:

function MyCtrl($scope, myApi: myApp.MyApi) {
    // ...
}

@mhegazy
Copy link
Contributor

mhegazy commented Aug 9, 2015

i would rather have a compiler operator e.g. __returnTypeof than to overload typeof.

@yortus
Copy link
Contributor

yortus commented Aug 9, 2015

If this facility existed as a purely compile-time thing, say __returnTypeof expr or decltype(expr) or whatever, could it be used in an ambient context? E.g:

declare module "mymodule" {
    import knex = require('knex');
    type QueryBuilder = __returnTypeof (knex(<any>{})()); // obtain the QueryBuilder type from knex;
    export function someExportedFunction(query: QueryBuilder): void;
}

This covers a likely use case for the construct, but would involve allowing expressions to appear in ambient declarations. Is that feasible with the design of the compiler?

@danquirk danquirk added Suggestion An idea for TypeScript Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. labels Aug 10, 2015
@rotemdan
Copy link

I think that the scope of typeof expression is very wide. It is very powerful but that also means that expressing some specific patterns in it would require sacrificing tidiness and clarity which would lead to lower general usability in those scenarios.

Perhaps it would be beneficial to try to isolate the most prominent cases where typeof expression would be commonly used (such as, say, typeon or returnTypeOf [and perhaps also returnTypeOn?] ) and try to individually consider a more "optimized", special-purpose syntax and semantics for each one of them.

It would still be useful to have a more powerful operator for special cases, however, it might be possible to emulate many of them by embedding the evaluated expression in a function and "extracting" the inferred return type, like this:

function expressionFunc() {
  return [...] // expression
}
// can also be written as 'expressionFunc = () => [..]'

var x: returnTypeOf expressionFunc;

This would naturally extend to generic expressions as well:

function genericExpressionFunc<T>() {
  return [...] // generic expression
}

var x: returnTypeOf expressionFunc<string>;

@benliddicott
Copy link

Can I add my vote to this item. This seems very useful and has wide application.

@d180cf
Copy link

d180cf commented Sep 11, 2015

Why not to introduce the notion of subtypes?

function f(x: number, y: string) {
  return x || y;
}

type T = (typeof f).ReturnType; // string | number

Every function type could have a .ReturnType member that would be equal to the type of what the function returns. Typescript knows this type, but doesn't seem to make it accessible in a straightforward way.

@rotemdan
Copy link

@d180cf

I think using a "virtual" sub-type/property is an interesting approach, and I'm also investigating alternatives to returnTypeOf and returnTypeOn myself (including even syntax that would handle overloaded return signatures etc.). The problem with this particular syntax is that Typescript (and Javascript) allows declaring hybrid function/object types (functions with normal properties defined on them):

interface HybridFunc {
  // This is a function signature specifier:
  (x: number, y: string): string;

  // This is an example of a normal property with type 'Array<boolean>'
  // that just happens to have the name 'ReturnType':
  ReturnType: Array<boolean>; 
}

let hybridFunc: HybridFunc;

So in this case type type T = typeof hybridFunc.ReturnType would be ambiguous.

Using it with my proposed typeon operator, (an operator that would allow referencing a property type directly through its containing type): would also have the same problem: type T = typeon HybridFunc.ReturnType.

@basarat
Copy link
Contributor

basarat commented Sep 14, 2015

Going though the issue. I suggest that a simpler workaround be used:

function myApiFactory() {
  return {
    getSetting: () => null
  };
}

const myApiFactoryReturn = false && myApiFactory();
export type MyApi = typeof myApiFactoryReturn;

Basically instead of

export type MyApi = typeof myApiFactory();

You have two lines:

const myApiFactoryReturn = false && myApiFactory();
type MyApi = typeof myApiFactoryReturn;

@rotemdan
Copy link

@basarat

I've found an even simpler workaround yesterday:

// This expression would never execute but its type would be inferred:
let e = true ? undefined : ...expression...;
type ExpressionType = typeof e;

To get return types:

let r = true ? undefined : someFunction();
type RerturnType = typeof r;

This workaround would only work on runtime code positions though, so it is mostly relevant totypeof func(). It can still be used to emulate typeon this.func() in some cases, but that would be more complex:

interface A {
  func(x: number): string;
  prop: ReturnType; // emulates 'typeon this.func()'
}

let a: A;
let r = true ? undefined : a.func(0);
type RerturnType = typeof r;

But similarly to other workarounds for typeon: in the case of a generic interface or class it would not be possible to propagate the generic parameter into the dummy instance:

interface A<T> {
  func(x: number): T;
  prop: typeon this.func(); // No workaround for this
}

@benliddicott
Copy link

See also Generic Meta Types #3779

@yortus
Copy link
Contributor

yortus commented Sep 14, 2015

@basarat @rotemdan At present typeof is useable in ambient contexts (e.g. in .d.ts files), and perhaps the extended typeof proposed here could preserve this quality. That's one thing the workarounds, consisting of imperative statements of various forms, cannot do.

@msklvsk
Copy link

msklvsk commented Dec 19, 2015

Dear Santa, please bring me decltype in Typescript.

@yortus
Copy link
Contributor

yortus commented Jan 25, 2016

I submitted a proposal + implementation for this: #6606

@RyanCavanaugh RyanCavanaugh added the Declined The issue was declined as something which matches the TypeScript vision label Jan 25, 2016
@RyanCavanaugh RyanCavanaugh removed the Needs Proposal This issue needs a plan that clarifies the finer details of how it could be implemented. label Jan 25, 2016
@RyanCavanaugh
Copy link
Member

Let's take the discussion over to the nice clean slate at #6606.

@magnushiie
Copy link
Contributor

magnushiie commented Jun 16, 2017

With recent null checks, the above recommendation:

let r = true ? undefined : someFunction();
type ReturnType = typeof r;

can be changed to

let r = true ? undefined as never : someFunction();
type ReturnType = typeof r;

to avoid the (undefined | ...) type.

@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests