Skip to content

Cannot escape distribution of conditional type properly #30020

Closed
@aoberoi

Description

@aoberoi

TypeScript Version: 3.4.0-dev.201xxxxx

Search Terms: distributive conditional types, 1-tuple

Code

// Here are a couple types that will populate a discriminated union
interface UnionA {
  type: 'a';
  foo: boolean;
}
interface UnionB {
  type: 'b';
  bar: number;
}

// This is the discriminated union
type Union = UnionA | UnionB;

// Here is a type that is like those in Union, but a little more general
interface MyDefault {
  type: string;
  /* ... more default properties */
}

// The goal is to have a type whose generic parameter which can identify a type by the  discriminant from
// Union, and if one isn't found, resolves to MyDefault

// First let's define a helper that will either resolve to T when its not the empty union, or
// Default when it is the empty union. Notice that the 1-tuple wrapper is used to avoid distribution.
type FallbackWhenBottom<T, Default> = [T] extends [never] ? Default : T;

// Now, this completes the goal.
type UnionByTypeWithDefault<T extends string> = FallbackWhenBottom<Extract<Union, { type: T }>, MyDefault>

// Let's test it out
type Test = UnionByTypeWithDefault<'a'>; // success: Test === UnionA
type Test2 = UnionByTypeWithDefault<'c'>; // fail: Test2 === never, expected Test2 === MyDefault

// DARN! Let's try to pick apart Test2 the way we think it should be evaluated

// This should be evaluated first (rem, FallbackWhenBottom was defined not to be distributive)
type Test3<T> = Extract<Union, { type: T }>;
type Test4 = Test3<'c'> // success: Test4 === never

// Then that should be substituted into FallbackWhenBottom
type Test5 = FallbackWhenBottom<never, MyDefault> // success: Test5 === MyDefault

// It seems like FallbackWhenBottom *is* behaving as distributive, as that's the only explanation
// for how Test2 could be never - T as never is being treated as the empty union. The 1-tuple should be preventing this ([never]), but it's not.

Expected behavior:

Test2 should evaluate to MyDefault

Actual behavior:

Test2 is evaluated to never

Playground Link: Link

Related Issues: #29368, #29627,

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