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

Union of partial types: common super type of all union members is not assignable to union type #33243

Closed
seansfkelley opened this issue Sep 4, 2019 · 1 comment · Fixed by #36663
Assignees
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue

Comments

@seansfkelley
Copy link

seansfkelley commented Sep 4, 2019

TypeScript Version: v3.5.1

Search Terms: union partial type assignable structure

Code

// Data types

interface Key {
  type: "key";
  key: string;
}

interface KeyValue {
  type: "key-value";
  key: string;
  value: string;
}

type Either = Key | KeyValue;

type Common = Pick<Either, "type" | "key">;

// Test cases

// Structurally-compatible assignment
const either: PartialEither = null as any as Common;

// Type-guarding
if (either.type === "key-value") {
  either.value;
}

// Problematic type definitions

// Using this intermediate type so I can still rely on 'type' as the discriminant property of
// the PartialEither type. The naive
// Pick<Either, "type" | "key"> & Partial<Omit<Either, "type" | "key">>
// would lose the relationship between `type` and shape, causing the type guard above to fail.
type _PartialEither<T extends Either> = Pick<T, "type" | "key"> & Partial<Omit<T, "type" | "key">>;

type PartialKey = _PartialEither<Key>;
type PartialKeyValue = _PartialEither<KeyValue>;

type PartialEither = PartialKey | PartialKeyValue;

// This type seems to force the compiler to reinterpret the partial types into a slightly
// different representation that's logically equivalent but properly assignable. From:
// https://stackoverflow.com/questions/57780109/union-of-partial-types-in-typescript-cant-be-type-narrowed
type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
// Change the definition to this version to see the test cases above work.
// type PartialEither = Expand<PartialKey | PartialKeyValue>;

Expected behavior: Line 21, the structurally-compatible assignment, compiles, without a workaround. Using the Expand type has no effect on assignability.

Actual behavior: Line 21 does not compile, stating

Type 'Pick<Either, "key" | "type">' is not assignable to type 'PartialEither'.
  Type 'Pick<Either, "key" | "type">' is not assignable to type '_PartialEither<KeyValue>'.
    Type 'Pick<Either, "key" | "type">' is not assignable to type 'Pick<KeyValue, "key" | "type">'.
      Types of property 'type' are incompatible.
        Type '"key" | "key-value"' is not assignable to type '"key-value"'.
          Type '"key"' is not assignable to type '"key-value"'.

Using the Expand helper type workaround fixes the assignability problem.

Playground Link: link

Related Issues: #18538, #19927

@seansfkelley seansfkelley changed the title Union of partial types is not assignable to common sub-type of all union members Union of partial types: common super type of all union members is not assignable to union type Sep 4, 2019
@jack-williams
Copy link
Collaborator

jack-williams commented Sep 4, 2019

My answer was a mess!

The reason why Expand works is probably because of this #30779; both approaches do not work pre 3.5. It seems that PartialEither as defined is not considered a discriminated union in that logic. TypeScript clearly understands that PartialEither is a discriminated union though, evidenced by:

if (either.type === "key-value") {
  either // narrowed
}

Smaller repro:

type X = 
 | { x: 'x', y: number } & { y: number } 
 | { x: 'y', y: number, z?: boolean } & { y: number }

// error
const x: X = 4 as any as { x: 'x' | 'y', y: number };

type Y = 
 | { x: 'x', y: number } 
 | { x: 'y', y: number, z?: boolean }

// no  error
const y: Y = 4 as any as { x: 'x' | 'y', y: number };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript Fix Available A PR has been opened for this issue
Projects
None yet
5 participants