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

Narrowing unions of interfaces with intradependent property types #51341

Closed
david-mancuso opened this issue Oct 28, 2022 · 3 comments
Closed
Labels
Duplicate An existing issue was already created

Comments

@david-mancuso
Copy link

david-mancuso commented Oct 28, 2022

Bug Report

Type narrowing doesn't behave as I expect it would for a union of interfaces where one property type is mapped directly to another property type. If this is not a bug, I would love to understand how I can better type this use case because it appears valid to me.

🔎 Search Terms

enum, union, type, interface, property, never, error, validation, gating, narrowing, 4.7.4, strict

🕗 Version & Regression Information

Bug appears in v4.7.4 (used in my context) through latest next version today.

⏯ Playground Link

Playground link with relevant code

💻 Code

enum FooKeys {
  BAR = 'bar',
  BAZ = 'baz'
}

interface Bar {
  key: FooKeys.BAR;
  value: string;
}

interface Baz {
  key: FooKeys.BAZ;
  value: number;
}

type Foo = Bar | Baz;

interface FooBar {
  bar: Bar['value'],
  baz: Baz['value'],
  opts: Foo[]
}

class FooBar implements FooBar {
  bar = 'abc';
  baz = 123;
  opts: Foo[] = [{
    key: FooKeys.BAR,
    value: 'def'
  },
  {
    key: FooKeys.BAZ,
    value: 456
  }
  ];

  constructor(){ } 

  foobar(): void {
    this.opts.forEach(({ key, value }) => {
      // Throws error:
      // Type 'string | number' is not assignable to type 'never'.
      // Type 'string' is not assignable to type 'never'.
      this[key] = value;
    });
  }
}

🙁 Actual behavior

The Foo properties of value and key are not narrowed relative to each other despite being constituents of discrete types where they are directly related.

🙂 Expected behavior

I would expect that typing an object as Foo would enforce the relationship between the key and value properties. This is because the Foo union is not an any to any relationship of its constituent interfaces' properties. Each interface has a type for the key which maps directly to a type for the value. These relationships are enforced at the time of initializing opts: Foo[] but that same reasoning seems to not apply when narrowing and gating the Foo objects.

@fatcerberus
Copy link

Duplicate of #35873 and/or #30581.

@RyanCavanaugh RyanCavanaugh added the Duplicate An existing issue was already created label Oct 28, 2022
@erabl-identos
Copy link

I've a similar usecase where a boolean property directly determines whether or not another property is either a valid enum value or null:

TS-Playground

enum InvalidReason {
  ERROR_1,
  ERROR_2,
}

interface EvaluatedDayBase {
  day: string;
  isValid: boolean;
  invalidReason: InvalidReason | null;
}

interface ValidEvaluatedDay extends EvaluatedDayBase {
  isValid: true;
  invalidReason: null;
}

interface InvalidEvaluatedDay extends EvaluatedDayBase {
  isValid: false;
  invalidReason: InvalidReason;
}

type EvaluatedDay = ValidEvaluatedDay | InvalidEvaluatedDay;


function getEvaluatedDay(): EvaluatedDay {
  let error: InvalidReason | null = null;

  // Types of property 'isValid' are incompatible. Type 'boolean' is not assignable to type 'false'.(2322)
  // with isValid = true and invalidReason = null, I would expect Typescript to see this as `ValidEvaluatedDay`
  return {
    day: '2022-10-31',
    isValid: error === null,
    invalidReason: error,
  };


  // below code works as expected though
  /* return {
    day: '2022-10-31',
    ...(error === null ? { isValid: true, invalidReason: null } : { isValid: false, invalidReason: error }),
  }; */
}

@typescript-bot
Copy link
Collaborator

This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Duplicate An existing issue was already created
Projects
None yet
Development

No branches or pull requests

5 participants