Skip to content

Discriminated union expands boolean to Foo<true> | Foo<false> instead of Foo<boolean> #46518

Closed

Description

Bug Report

🔎 Search Terms

Discriminated unions, generic unions, union expansion, boolean expands to true | false

🕗 Version & Regression Information

Tested on 4.4.2, nightly and 3.9.10.

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about generics, unions, union expansion

Possibly related: #22596, #22630, #30029.

⏯ Playground Link

Playground link with relevant code

💻 Code

export interface ObservableBase<T> {
    (): T; // Read the current value
    (newValue: T): void; // Write a new value
    value: T;
}

export type Observable<T> = T extends any[] ? ObservableArray<T> : ObservableBase<T>;

interface ObservableArray<T extends any[]> extends ObservableBase<T> {
    /** Array push. */
    push: (arg: T[number]) => void;
}

function makeObservable<T>(initial: T): Observable<T> {
    return {
        value: initial,
    } as Observable<T>;
}

const x = true;
const obs = makeObservable(x);

export function foo(z: boolean) {
    obs(z); // write a new value (should be `(boolean) => void`)
    obs(); // read the current value (should be `() => boolean`)
}

🙁 Actual behavior

No overload matches this call.
  Overload 1 of 3, '(newValue: false): true | void', gave the following error.
    Argument of type 'boolean' is not assignable to parameter of type 'false'.
  Overload 2 of 3, '(newValue: true): false | void', gave the following error.
    Argument of type 'boolean' is not assignable to parameter of type 'true'.

Note that obs has a type of ObservableBase<false> | ObservableBase<true>. This doesn't seem right to me. I made an observable that could accept values of true or false, not an observable that only accepts true or an observable that only accepts false.

I was sure I remember seeing something in the TypeScript documentation that allowed a bit more control over when union types are expanded for generics that would prevent this kind of thing, but I have searched high and low and cannot find anything.

If you change makeObservable to return an ObservableBase<T> instead of an Observable<T> then obs correctly gets a type of ObservableBase<boolean> and everything works. But the discriminated union seems to break everything.

🙂 Expected behavior

The above should not have any type errors.

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

      Issue actions