Skip to content

Type assertion on unspecified generic return type changes the type #36062

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

Closed
bradzacher opened this issue Jan 7, 2020 · 6 comments
Closed

Type assertion on unspecified generic return type changes the type #36062

bradzacher opened this issue Jan 7, 2020 · 6 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@bradzacher
Copy link
Contributor

TypeScript Version: nightly (on playground).

Search Terms:
generic, return, assertion, inference, checker api.

Code

declare function genericGet<TValue>(object: any, path: string): TValue

const object = { foo: { bar: 123 } };
const result1 = genericGet(object, 'foo.bar');
const result2 = genericGet(object, 'foo.bar') as number;

Expected behavior:
In both cases, the return type of genericGet(object, 'foo.bar') reported by the type checker API (checker.getTypeAtLocation) should be unknown.

Actual behavior:
In the first case, the return type of genericGet(object, 'foo.bar') reported by the type checker API is unknown.
image

In the second case, the return type is number.
image

(best shown via ts-ast-viewer, link below).

Playground Link:
https://www.typescriptlang.org/play/?ssl=1&ssc=1&pln=7&pc=56#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwHMRUQYswBxEDAHgBUA1KCZEAPgAocAjAK3AwAueFFQBPADTwADlAwALYQGcMZVAQCUwxs1YAoPWDwr4PfpngBeeAG8kOHMLvdYwgIwAmAMzwAvn4BuQ2MMeDglZAgMNytCYlJyKgwuPgEpAHJEBwA6Fxh0jSCjVBNwyIwPWKISMkpqFPMMDKycXNgCkSV4VGQAW25SAKA

https://ts-ast-viewer.com/#code/CYUwxgNghgTiAEAzArgOzAFwJYHtXwHMRUQYswBxEDAHgBUA1KCZEAPgAocAjAK3AwAueFFQBPADTwADlAwALYQGcMZVAQCUwxs1YAoPWDwr4PfpngBeeAG8kOHMLvdYwgIwAmAMzwAvn4BuQ2MMeDglZAgMNytCYlJyKgwuPgEpAHJEBwA6Fxh0jSCjVBNwyIwPWKISMkpqFPMMDKycXNgCkSV4VGQAW25SAKA

Related Issues:
#31435
Loosely related to: #35431

Related issue in typescript-eslint:
typescript-eslint/typescript-eslint#1410

Related rule logic:
https://github.com/typescript-eslint/typescript-eslint/blob/aee723813ec47ccac0a165cf1bc9674f6257b609/packages/eslint-plugin/src/rules/no-unnecessary-type-assertion.ts#L244-L292


I found #31435, which seems to suggest that this is entirely intentional.

However I would like to question this from the context of the type checker API.
This makes the type API pretty hard to use for certain use cases. Inspecting the type of the assertion expression (via checker.getTypeAtLocation) says it's of type number, and similarly inspecting the type of the call expression via the same API says it's of type number.

Is there any way to get the type that hasn't been inferred from a return type assertion?

In the context of our no-unnecessary-type-assertion rule, the rule warns about assertions that don't do anything, so that there is less useless code in a codebase..
(i.e. genericGet(object, 'foo.bar') as number is necessary, because it asserts unknown to number, but genericGet<number>(object, 'foo.bar') as number is unnecessary, because the return type is already number).

@RyanCavanaugh
Copy link
Member

There isn't a way to do this; the type of the expression really truly actually for real is number, and an unknown is never produced during the inference process for TValue. We don't have any concept of "what would the type have been were it not for the surrounding context of the expression", which seems to be what you'd really need to assess the underlying intent of the rule.

Hopefully this has the end effect of dissuading people from writing functions like genericGet in the first place?

@nevir
Copy link

nevir commented Jan 7, 2020

A real world example of this is lodash.get, unfortunately:

(edit: older versions of lodash typings, that is)

interface lodashGet<TObject, TResult> {
  (object: TObject, path: string | string[], defaultValue?: TResult): TResult;
}
const result = lodashGet(object, 'foo.bar') as number;

@nevir
Copy link

nevir commented Jan 7, 2020

Overrides are one way to work around that specific one:

function lodashGet<TObject, TResult>(object: TObject, path: string | string[]): unknown;
function lodashGet<TObject, TResult>(object: TObject, path: string | string[], defaultValue: TResult): TResult;
function lodashGet<TObject, TResult>(object: TObject, path: string | string[], defaultValue?: TResult): TResult {
  // pretend this works
}

…but I'm wary of having to patch common libraries when they have generic return types that are optional like that

@bradzacher
Copy link
Contributor Author

bradzacher commented Jan 7, 2020

I was trying to find that definition, but it looks like the types for lodash's get method have been somewhat refined so that it doesn't use the "generic that is only used in the return position" anti-pattern.

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/56c1ea26b59ed0e4634b1ba27096ab3b90371875/types/lodash/common/object.d.ts#L1659-L1792

@nevir
Copy link

nevir commented Jan 7, 2020

Oh, nice find! I can upgrade my type declarations to mitigate that instance then 👍

Seems likely to come up in other contexts though?

@typescript-bot
Copy link
Collaborator

This issue has been marked as 'Question' and has seen no recent activity. It has been automatically closed for house-keeping purposes. If you're still waiting on a response, questions are usually better suited to stackoverflow.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Question An issue which isn't directly actionable in code
Projects
None yet
Development

No branches or pull requests

4 participants