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

generic type deduction affected by unrelated factors #26946

Closed
albert-mirzoyan opened this issue Sep 6, 2018 · 4 comments
Closed

generic type deduction affected by unrelated factors #26946

albert-mirzoyan opened this issue Sep 6, 2018 · 4 comments
Labels
Duplicate An existing issue was already created

Comments

@albert-mirzoyan
Copy link

TypeScript Version: 3.1.0-dev.201808331

Search Terms: generic type deduction

The generic type deduction does something odd. The type deduction should not be affected by extends like it does in this example. Also type "magic" also affects the behavior while all the parts do what they suppose to do.

Code

class Some {
    foo() { }
    bar() { }
    some:number
    other:string
}
type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T];
type T1 = FunctionPropertyNames<Some> //this is "foo" | "bar" which is good. 
interface Result<T> {
    test: T
}
function fun1<E, P extends FunctionPropertyNames<E>>(ctor: {new(...args: any[]): E}, prop: P): Result<P> {
    return ;
}
function fun2<E, P extends keyof E>(ctor: { new(...args: any[]): E }, prop: P): Result<P> {
    return;
}
function fun3<E, P extends "foo"|"bar">(ctor: { new(...args: any[]): E }, prop: P): Result<P> {
    return;
}
let k1 = fun1(Some, "foo") //infer Result<"foo" | "bar"> WHY?
let k2 = fun2(Some, "foo") //infer Result<"foo">
let k3 = fun1(Some, "foo" as "foo") //infer Result<"foo">. Really?
let k4 = fun3(Some, "foo") //infer Result<"foo">

Expected behavior:
k1 should be Result<"foo">

Actual behavior:
k1 deduced to be Result<"foo" | "bar">

Playground Link:
Playground Link
Related Issues:

@mattmccutchen
Copy link
Contributor

mattmccutchen commented Sep 6, 2018

I believe this is a design limitation. TypeScript's heuristic is that a fresh literal type (here "foo") inferred for a type parameter is widened to the primitive type unless that type parameter has a constraint involving primitive or keyof types. (See hasPrimitiveConstraint in the checker.) fun2 and fun3 meet the condition to skip widening, while fun1 doesn't. To make fun1 work, you could intersect FunctionPropertyNames<E> with keyof E, which will make the inference work without changing the final constraint once E is known.

@albert-mirzoyan
Copy link
Author

@mattmccutchen Thank you for the workaround.
IMHO if the heuristics understands that the literal type "foo" should be widened to ("foo"|"bar") and not to (string) then it should skip widening. I don't see any sense in widening to ("foo"|"bar"), to (string) yes definitely.

@mattmccutchen
Copy link
Contributor

What I believe is happening is that the candidate "foo" for P is being widened to string and then being discarded because it doesn't satisfy the evaluated constraint FunctionPropertyNames<E> = "foo" | "bar". There are no candidates left, so the evaluated constraint "foo" | "bar" is used. We could debug the compiler to be sure.

@ahejlsberg
Copy link
Member

This is a duplicate of #25215. As @mattmccutchen explains above, the issue is that the FunctionPropertyNames<T> type isn't considered a primitive type, so the string literal is widened during type inference. Intersecting with string as suggested in #25215 fixes the issue:

type FunctionPropertyNames<T> = { [K in keyof T]: T[K] extends Function ? K : never }[keyof T] & string;

@ahejlsberg ahejlsberg added the Duplicate An existing issue was already created label Sep 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

3 participants