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

Unable to infer type from union with undefined when using mapped types #16943

Closed
raveclassic opened this issue Jul 5, 2017 · 6 comments
Closed

Comments

@raveclassic
Copy link

raveclassic commented Jul 5, 2017

I'm getting some wierd behavior when trying to match possibly undefined type of a value from a mapped type with a generic union including undefined. strictNullChecks flag is on.
The code below better describes the problem.

TypeScript Version: 2.4.1

Code

// A *self-contained* demonstration of the problem follows...
type TStorage = {
    a?: number,
    b?: string
};
const storage: TStorage = {
    a: 1,
    b: '2'
};
//this was original function signature with return type 
//but was commented to better reflect the problem with type inference
//function get<K extends keyof TStorage>(key: K): TStorage[K] {
function get<K extends keyof TStorage>(key: K) {
    //inferred type of result
    // const result: {
    //     a?: number | undefined,
    //     b?: number | undefined
    // }[K]
    const result = storage[key];
    return fromUndefined(storage[key]);
}

function fromUndefined<T>(value: T | undefined): T {
    return value!; //cast here is just for example
}

declare const b: string | undefined;
//const bb: string - correct
const bb = fromUndefined(b);

//const value: number | undefined - correct
const value = storage.a;
//const value2: number - also correct
const value2 = fromUndefined(storage.a);

//const a: string | undefined <----  Why undefined?
const a = get('b');

Expected behavior:

last const a is of type string

Actual behavior:

last const a is of type string | undefined


Is it correct behavior and I'm missing something about mapped types?

@ikatyang
Copy link
Contributor

ikatyang commented Jul 5, 2017

Optional parameter is marked as T | undefined, so that TStorage[K] is string | undefined indeed.

type TStorage = {
    a?: number,
    b?: string
};
type A = TStorage['a'] //=> number | undefined
type B = TStorage['b'] //=> string | undefined
type TStorageValue = TStorage[keyof TStorage] //=> string | number | undefined

The following codes are considered equivalent on type-level, since their return types are specified TStorage[K].

declare function get<K extends keyof TStorage>(key: K): TStorage[K];
function get<K extends keyof TStorage>(key: K): TStorage[K] {
    const result = storage[key];
    return fromUndefined(storage[key]);
}

@raveclassic
Copy link
Author

raveclassic commented Jul 5, 2017

I see.
And if return type annotation is removed from get shouldn't it then infer correct type?

function get<K extends keyof TStorage>(key: K) /* No return type */ {
    const result = storage[key];
    return fromUndefined(storage[key]);
}

Looks like result's type TStorage[K] is not processed by fromUndefined.

UPDATE: fromUndefined works correct when using simple not mapped types (shown in original code above)

@raveclassic
Copy link
Author

raveclassic commented Jul 5, 2017

Seems like result's type

// const result: {
//     a?: number | undefined,
//     b?: number | undefined
// }[K]

is not evaluated when passing to fromUndefined and therefore does not contain expected | undefined

That's why I'll try to rephrase the question - are there any plans to support such functionality or it's an intentional design decision?

@raveclassic
Copy link
Author

I have also updated original code with commenting get return type to better reflect the problem.

@ikatyang
Copy link
Contributor

ikatyang commented Jul 5, 2017

fromUndefined is just a kind of workaround for type subtraction, there are some limitations for it:

fromUndefined(undefined); //=> expected never, but got undefined

function something<K extends keyof TStorage>(key: K) {
  const result = storage[key]; //=> TStorage[K]
  fromUndefined(storage[key]); //=> TStorage[K]
}

For those real type subtraction, see #4183.

@raveclassic
Copy link
Author

Thanks for the anwser. #4183 contains lots of useful links to similar issues so I'm going to close this.

@microsoft microsoft locked and limited conversation to collaborators Jun 14, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants