1

I have two examples where TypeScript type inference is not working as I expect it, maybe these two are related, maybe it's a TS bug, maybe I am only too dumb to understand it.

Example #1

interface AType {
  type: 'A'
  message: string
}

interface BCType {
  type: 'B' | 'C'
}

type Test = AType | BCType

function doSomething(test: Test) {
  if (test.type !== 'B' && test.type !== 'C') {
    test.message
  }
}

The test.message gets the error

Property 'message' does not exist on type 'Test'.
  Property 'message' does not exist on type 'BCType'.ts(2339)

Why isn't test of AType now, so it has a message property? If I remove the 'C' from the type, it works. If I check for === 'A', is also works.

Example #2

interface AType {
  type: {
    id: 'A'
  }
  message: string
}

interface BType {
  type: {
    id: 'B'
  }
}

type Test = AType | BType

function doSomething(test: Test) {
  if (test.type.id === 'A') {
    test.message
  }
}

Same here:

Property 'message' does not exist on type 'Test'.
  Property 'message' does not exist on type 'BType'.ts(2339)
1

1 Answer 1

1

Type guards generally work on the field of variable they are applied to, so if you narrow test.type, the narrowing generally applies to just type not to test. The one exception to this is if the type of test is a discriminated union and test is a the discriminant field.

For the property to be a discriminant property, it must fulfill some requirements:

  1. The property is a literal type as outlined here Discriminated union types
  2. The a property of a union type to be a discriminant property if it has a union type containing at least one unit type and no instantiable types as outlined here Allow non-unit types in union discriminants

As for the second example, there just isn't support for nested discriminated unions. There is a proposal to support this but it has been open for 3 years, without much movement.

You can adjust your model to conform to discriminated union rules, or, as a work around, you can use custom type guards:

interface AType {
  type: 'A'
  message: string
}

interface BCType {
  type: 'B' | 'C'
}

type Test = AType | BCType

function doSomething(test: Test) {
  if (!isBC(test)) {
    test.message
  }
}

function isBC(o: any): o is BCType {
  return o?.type != "B" && o?.type != "C"
}

Playground Link

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you, I didn't know that the topic is that specific, but good to know, and your explanation is good to understand.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.