Skip to content

Wrong type inferred for nested union #20280

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
dwickern opened this issue Nov 27, 2017 · 4 comments
Closed

Wrong type inferred for nested union #20280

dwickern opened this issue Nov 27, 2017 · 4 comments
Labels
Question An issue which isn't directly actionable in code

Comments

@dwickern
Copy link

TypeScript Version: 2.7.0-dev.20171126

Context: a method for ember.js which extracts the value from some path, e.g.

get({ foo: { bar: { baz: 'hello' }}}, 'foo', 'bar', 'baz') // returns "hello"

Each value in the path might be some plain object T or a Wrapped<T> (in which case we return the underlying T)

Code

Typescript can infer the returned type using a union:

// working example
interface Wrapped<T> { value: T }

type Props1<T> = {
    [K in keyof T]: Wrapped<T[K]> | T[K];
}

declare function get1<T, K1 extends keyof T>(obj: Props1<T>, k1: K1): T[K1];

declare const obj1: { a: string };
const v1: string = get1(obj1, 'a'); // works

declare const obj2: { a: Wrapped<string> };
const v2: string = get1(obj2, 'a'); // works

However, it no longer works when the union types are nested:

// non-working example
type Props2<T> = {
    [K in keyof T]: Wrapped<Props1<T[K]>> | Props1<T[K]>
};

declare function get2<T, K1 extends keyof T, K2 extends keyof T[K1]>(obj: Props2<T>, k1: K1, k2: K2): T[K1][K2];

declare const obj3: { a: { b: string } };
const v3: string = get2(obj3, 'a', 'b'); // works

declare const obj4: { a: Wrapped<{ b: Wrapped<string> }> };
const v4: string = get2(obj4, 'a', 'b'); // TS2345:Argument of type '"b"' is not assignable to parameter of type 'never'.

If I eliminate the right side of the union from Props2, then the obj4 example works

type Props2<T> = {
    [K in keyof T]: Wrapped<Props1<T[K]>> //| Props1<T[K]>
};

The obj4 example should pick the left side of the union to begin with

@chriskrycho
Copy link

To add a bit, the motivation is providing a type-safe wrapper around Ember.get(someObj, 'a.b'). We want to be able to do a safeGet(someObj, 'a', 'b') which simply dispatches internally to the relevant path, but which does type-checking on the supplied keys – for an API that's slightly less ergonomic than the built-in Ember API, but which is much nicer than doing it the way we are presently.

// current way to do it to get type inference
const a = get(someObj, 'a');
const b = get(a, 'b');

// desired API
const b = safeGet(someObj, 'a', 'b');

@RyanCavanaugh RyanCavanaugh added the Question An issue which isn't directly actionable in code label Aug 7, 2018
@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Aug 7, 2018

@dwickern @chriskrycho I wrote a version using conditional types that works:

type Unwrap<T> = T extends Wrapped<infer U> ? U : T;

// working example
interface Wrapped<T> {
  value: T
}

type Props1<T> = {
    [K in keyof T]: T[K];
}

declare function get1<T, K1 extends keyof T>(obj: Props1<T>, k1: K1): Unwrap<T[K1]>;

declare const obj1: { a: string };
const v1: string = get1(obj1, 'a'); // works

declare const obj2: { a: Wrapped<string> };
const v2: string = get1(obj2, 'a'); // works

type Props2<T> = {
    [K in keyof T]: Unwrap<Props1<T[K]>>
};

declare function get2<T, K1 extends keyof T, K2 extends keyof Unwrap<T[K1]>>(obj: Props2<T>, k1: K1, k2: K2): Unwrap<Unwrap<T[K1]>[K2]>;

declare const obj3: { a: { b: string } };
const v3: string = get2(obj3, 'a', 'b'); // works

declare const obj4: { a: Wrapped<{ b: Wrapped<string> }> };
const v4: string = get2(obj4, 'a', 'b'); // now works

I don't think this is solvable otherwise because we don't preferentially go down one path or the other when inferring a key type for the union.

@chriskrycho
Copy link

Ah, this is genuinely great. Thank you, @RyanCavanaugh. I don’t have time to experiment tonight; it looks like it should be possible to extend this using overloads but probably not tuple/argument types, if I’m tracking correctly?

At a minimum, it’ll give us a tool we can build on for this. Thanks for circling back around to it!

@dwickern
Copy link
Author

dwickern commented Oct 5, 2018

This is possible now with conditional types 🎉

@dwickern dwickern closed this as completed Oct 5, 2018
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

3 participants