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

Assertion functions cannot be used to narrow discriminated union types #37241

Closed
oleg-codaio opened this issue Mar 6, 2020 · 3 comments · Fixed by #37310
Closed

Assertion functions cannot be used to narrow discriminated union types #37241

oleg-codaio opened this issue Mar 6, 2020 · 3 comments · Fixed by #37310
Assignees
Labels
Bug A bug in TypeScript

Comments

@oleg-codaio
Copy link

oleg-codaio commented Mar 6, 2020

Recently I tried to get creative with an assert function, but ran into a shortcoming. I think I understand why this is the behavior, but it would be nice if it wasn't the case.

TypeScript Version: 3.9.0-dev.20200304

Search Terms: assertion assert function discriminated union narrowing

Code

interface Cat {
  type: 'cat';
  canMeow: true;
}

interface Dog {
  type: 'dog';
  canBark: true;
}

type Animal = Cat | Dog;

function assertEqual<T>(value: any, type: T): asserts value is T {
  if (value !== type) {
    throw new Error('Not equal!');
  }
}

const animal = { type: 'cat', canMeow: true } as Animal;
assertEqual(animal.type, 'cat' as const);

// Error
animal.type; // "cat"
animal.canMeow; // Property 'canMeow' does not exist on type 'Dog'

// Works
if (animal.type !== 'cat') {
  throw new Error();
}
animal.canMeow; // true

Expected behavior:
Calling assertEqual(animal.type, 'cat') would infer animal as a Cat.

Actual behavior:
Only animal.type by itself is inferred; the type narrow does not "propagate" up to the union type.

Playground Link: Provided

Related Issues: #11787 (most closely related, but that's for type guards, not the new assertion functions), #34596

@oleg-codaio oleg-codaio changed the title Assertion functions cannot be used to narrow union types Assertion functions cannot be used to narrow discriminated union types Mar 6, 2020
@jamietre
Copy link

jamietre commented Mar 6, 2020

I'm having a similar problem. It's not totally clear to me if your example exposes the same behavior, but it seems very similar: the type is not narrowed by the assertion.

interface Ok extends String {
  _nominal: null;
}

function assertIsOk(foo: string | Ok): asserts foo is Ok {
  if (foo !== "ok") {
    throw new Error("not ok");
  }
}

function acceptsOnlyString(foo: string) {
  ...
}

function checkIfOk(baz: string) {
  assertIsOk(baz);

  // compiles  - `baz` is now type "string & Ok"; expected it to be type "Ok" and would fail
  acceptsOnlyString(baz);
}

Works the same way when coded as a type guard.

@RyanCavanaugh RyanCavanaugh added Working as Intended The behavior described is the intended behavior; this is not a bug and removed Working as Intended The behavior described is the intended behavior; this is not a bug labels Mar 9, 2020
@RyanCavanaugh RyanCavanaugh added the Bug A bug in TypeScript label Mar 9, 2020
@RyanCavanaugh RyanCavanaugh added this to the TypeScript 3.9.0 milestone Mar 9, 2020
@BobElDevil
Copy link

BobElDevil commented Mar 9, 2020

Note for a workaround in the first example, a more generic asserts value works for the original example situation, since that asserts the entirety of the statement as true, like an 'if' would

interface Cat {
  type: 'cat';
  canMeow: true;
}

interface Dog {
  type: 'dog';
  canBark: true;
}

type Animal = Cat | Dog;

function assert(value: any): asserts value {
  if (!value) {
    throw new Error();
  }
}

const animal = { type: 'cat', canMeow: true } as Animal;
assert(animal.type === 'cat');

animal.type; // "cat"
animal.canMeow; // Works

@wmaca
Copy link

wmaca commented Mar 18, 2020

It seems to be affecting other union types, as the code below demonstrates.

Code

type MyUnion = { [key: string]: any } | string[];

function assertObject(value: any): asserts value is Record<string, any> {
    if (value === null || typeof value !== "object" || Array.isArray(value)) {
        throw new Error("Assertion failed");
    }
}

function getValue(): MyUnion {
    return { foo: "bar" };
}

const value = getValue();

assertObject(value);

// Error as it does not know if it is a string[] or { [key: string]: any }
console.log(value["propName"]); 

Playground Link: here

More details

I've also observed that TypeScript is using the already known type at caller (i.e. MyType) to narrow after the assert function is called. This is resulting in never as a result.

I am not sure this part is expected.

type MyType =
	| {
			[key: string]: {
				[k: string]: number;
			};
	  }
	| string;

const a: MyType = { foo: { bar: 123 } };

function assertUndefined(value: any): asserts value is undefined {
	if (value !== undefined) throw new Error();
}

assertUndefined(a.foo);

// `a.foo` is `never`
console.log(a.foo);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bug A bug in TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants