Skip to content

Narrowing Union of objects does not return a Union of different narrowed types #54041

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
yannbriancon opened this issue Apr 27, 2023 · 4 comments

Comments

@yannbriancon
Copy link

Bug Report

When modifying a variable with a type Union of objects, the inferred type should be the Union of the possible results.

🔎 Search Terms

  • Union
  • Narrow union

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about Union and narrow

⏯ Playground Link

Playground link with relevant code

💻 Code

type Source = {
    type: 'ws'
    reader: {state: 'connecting'}
} | {
    type: 'file'
    reader: {state: number}
}

let source: Source = {type: 'file', reader:  {state: 12}}

/*
 * Error: getSourceWithState return type is 
 * {
 *   type: "ws" | "file";
 *   state: "connecting" | number;
 * } 
 * instead of
 *  {
 *   type: "file";
 *   state: number;
 * } |
 *  {
 *   type: "ws";
 *   state: "connecting";
 * } 
 */
const getSourceWithState = () => {
    return {
        type: source.type,
        state: source.reader.state
    }
}

const sourceWithState = getSourceWithState()
if (sourceWithState.type === 'file') {
    // Error
    sourceWithState.state += 1
}

🙁 Actual behavior

The return type of getSourceWithState is

{
  type: "ws" | "file";
  state: "connecting" | number;
} 

Then trying to narrow the type is impossible.

🙂 Expected behavior

The return type of getSourceWithState should be

{
  type: "file";
  state: number;
} |
{
  type: "ws";
  state: "connecting";
} 

This would make it possible to narrow the type and is also the truth.

@fatcerberus
Copy link

fatcerberus commented Apr 27, 2023

I think this falls under the same limitation I described here #54027 (comment) - the compiler doesn’t track where individual values come from (see #30581)

@jcalz
Copy link
Contributor

jcalz commented Apr 27, 2023

This feels a lot like #30581; TS doesn't do "distributive control flow analysis" (see #25051). Generally the refactoring needed to get behavior like this is to use generic indexes into mapped types as described in #47109. Doing that here gives me

interface SourceMap {
    ws: "connecting";
    file: number;
}
type Source<K extends keyof SourceMap = keyof SourceMap> =
    { [P in K]: { type: P, reader: { state: SourceMap[P] } } }[K]

type SourceWithState<K extends keyof SourceMap = keyof SourceMap> =
    { [P in K]: { type: P, state: SourceMap[P] } }[K]

function getWithState<K extends keyof SourceMap>(s: Source<K>): SourceWithState<K> {
    const type: K = s.type;
    const state: SourceMap[K] = s.reader.state;
    return { type, state };
}

let source: Source = { type: 'file', reader: { state: 12 } }

const getSourceWithState = () => getWithState(source);

const sourceWithState = getSourceWithState()
if (sourceWithState.type === 'file') {
    sourceWithState.state += 1 // okay
}

Playground link

Which works but is much less ergonomic than just throwing a type assertion at the problem and moving on. I don't know if there will be anything better here.

@yannbriancon
Copy link
Author

Thanks for your replies @jcalz & @fatcerberus

@jcalz It is exactly what I do usually but I am tired of this complicated mapping and hoped something better would exist.

Hope someday this feature can be implemented.

@fatcerberus
Copy link

fatcerberus commented Apr 27, 2023

@jcalz For what it's worth I'm not convinced going all-in on distributive CFA is strictly necessary - most of these cases feel like they could be solved by simply tracking the provenance of types. That would allow the compiler to recognize that

const { x, y } = p;
const q = { x, y };

is an isomorphism regardless of the types involved, without having to distribute over any unions (except to construct the final type of the object literal, where it'd distribute over the type of p).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants