Skip to content

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

Closed
@yannbriancon

Description

@yannbriancon

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.

Activity

fatcerberus

fatcerberus commented on Apr 27, 2023

@fatcerberus

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

jcalz commented on Apr 27, 2023

@jcalz
Contributor

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

yannbriancon commented on Apr 27, 2023

@yannbriancon
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

fatcerberus commented on Apr 27, 2023

@fatcerberus

@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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

      Development

      No branches or pull requests

        Participants

        @jcalz@fatcerberus@yannbriancon

        Issue actions

          Narrowing Union of objects does not return a Union of different narrowed types · Issue #54041 · microsoft/TypeScript