How to fix Discriminant property 'X' cannot be used for narrowing in TypeScript

TypeScriptINTERMEDIATEMEDIUMUpdated Dec 8, 2025· 94 views

This TypeScript error occurs when a discriminant property in a union type cannot effectively narrow the type due to issues like non-literal types, assignable types overlapping, or intersection conflicts. Proper discriminated unions require literal type discriminants and non-overlapping type members.

Quick fix

Fixes Discriminant property 'X' cannot be used for narrowing

// WRONG - discriminant uses general string type
type BadResult =
  | { status: string; data: string }
  | { status: string; error: Error };

function process(result: BadResult) {
  if (result.status === "success") {
    // TypeScript cannot narrow - 'data' might not exist
    console.log(result.data); // Error
  }
}

// CORRECT - discriminant uses literal types
type GoodResult =
  | { status: "success"; data: string }
  | { status: "error"; error: Error };

function processGood(result: GoodResult) {
  if (result.status === "success") {
    // TypeScript narrows successfully
    console.log(result.data); // OK - knows this is the success type
  }
}
  1. Use literal types for discriminant properties
  2. Ensure union members are not assignable to each other
  3. Avoid intersections that break discriminant recognition
  4. Narrow from the correct initial type
typescript — error
// WRONG - discriminant uses general string type
type BadResult =
| { status: string; data: string }
| { status: string; error: Error };
function process(result: BadResult) {
if (result.status === "success") {
// TypeScript cannot narrow - 'data' might not exist
console.log(result.data); // Error
}
}
// CORRECT - discriminant uses literal types
type GoodResult =
| { status: "success"; data: string }
| { status: "error"; error: Error };
function processGood(result: GoodResult) {
if (result.status === "success") {
// TypeScript narrows successfully
console.log(result.data); // OK - knows this is the success type
}
}
Discriminant property 'X' cannot be used for narrowing

What this error means

TypeScript's discriminated unions are a powerful pattern for working with union types that share a common literal property (the discriminant). When you check the discriminant property in a conditional, TypeScript can narrow the union to the specific type that matches the discriminant value. However, TypeScript cannot perform this narrowing when: 1. **The discriminant property is not a literal type**: Using `string` or `boolean` instead of specific literal values like `"success"` or `true` prevents narrowing 2. **Types in the union are assignable to each other**: When one type's shape can satisfy another type in the union, TypeScript cannot definitively narrow 3. **The discriminant property appears in intersections or complex type manipulations**: Type operations can break the compiler's ability to recognize the discriminant pattern 4. **The discriminant is accessed from a wider union before narrowing**: When narrowing from a broader type (like `Record<string, any> | MyUnion`), the discriminant may not work correctly This error typically manifests as TypeScript refusing to narrow a union type even when you've checked the discriminant property, leaving you with the full union type instead of the specific narrowed type you expected.

Typical symptoms

  • TypeScript doesn't narrow the union type after checking the discriminant property
  • The type remains as the full union instead of narrowing to a specific member
  • IDE shows properties are possibly undefined even after discriminant check
  • Type guards on discriminant properties have no effect on the resulting type
  • Error appears when accessing properties specific to one union member after checking discriminant
  • Code that works at runtime fails TypeScript's type checking

Common causes

  1. Discriminant property uses non-literal types (string, number, boolean instead of 'success', 42, true)
  2. One type in the union is structurally assignable to another (overlapping shapes)
  3. Discriminated union is wrapped in an intersection type that breaks narrowing
  4. Variable was initially typed as a wider union (like Record<string, any> | YourUnion)
  5. Discriminant property has union type values instead of single literal values per member
  6. Complex type aliases or generic constraints obscure the discriminant pattern
  7. Discriminant property is optional in some union members
  8. Inconsistent discriminant property names or values across union members

How to fix "Discriminant property 'X' cannot be used for narrowing"

1Use literal types for discriminant properties

The discriminant property must use literal types, not general types like string or boolean:

typescript
// WRONG - discriminant uses general string type
type BadResult =
  | { status: string; data: string }
  | { status: string; error: Error };

function process(result: BadResult) {
  if (result.status === "success") {
    // TypeScript cannot narrow - 'data' might not exist
    console.log(result.data); // Error
  }
}

// CORRECT - discriminant uses literal types
type GoodResult =
  | { status: "success"; data: string }
  | { status: "error"; error: Error };

function processGood(result: GoodResult) {
  if (result.status === "success") {
    // TypeScript narrows successfully
    console.log(result.data); // OK - knows this is the success type
  }
}

Each union member must have a unique literal value for the discriminant property.

2Ensure union members are not assignable to each other

If one type in your union can satisfy another type's structure, TypeScript cannot narrow reliably:

typescript
// WRONG - Person is assignable to Employee (all Employee properties exist on Person)
type Employee = { name: string; role: string };
type Person = { name: string; role: string; age: number };

type BadUnion = Employee | Person;

// CORRECT - use discriminant to make types mutually exclusive
type GoodEmployee = { kind: "employee"; name: string; role: string };
type GoodPerson = { kind: "person"; name: string; role: string; age: number };

type GoodUnion = GoodEmployee | GoodPerson;

function identify(entity: GoodUnion) {
  if (entity.kind === "employee") {
    // Narrowed to GoodEmployee
    console.log(entity.name, entity.role);
  } else {
    // Narrowed to GoodPerson
    console.log(entity.name, entity.age);
  }
}

Add a discriminant property with unique literal values to make the types mutually exclusive.

3Avoid intersections that break discriminant recognition

Intersecting a discriminated union with other types can prevent TypeScript from recognizing the discriminant pattern:

typescript
type Action =
  | { type: "increment"; amount: number }
  | { type: "decrement"; amount: number };

// WRONG - intersection breaks narrowing
type TimestampedAction = Action & { timestamp: Date };

function handle(action: TimestampedAction) {
  if (action.type === "increment") {
    // May not narrow correctly depending on TypeScript version
    console.log(action.amount);
  }
}

// CORRECT - add timestamp to each union member individually
type BetterAction =
  | { type: "increment"; amount: number; timestamp: Date }
  | { type: "decrement"; amount: number; timestamp: Date };

function handleBetter(action: BetterAction) {
  if (action.type === "increment") {
    // Narrows correctly
    console.log(action.amount, action.timestamp);
  }
}

Distribute shared properties across all union members instead of using intersections.

4Narrow from the correct initial type

When narrowing a variable that starts as a wider union, the discriminant may not work:

typescript
type MyData =
  | { type: "text"; content: string }
  | { type: "image"; url: string };

// WRONG - wider initial type prevents narrowing
type ApiResponse = Record<string, any> | MyData;

function process(response: ApiResponse) {
  if (typeof response === "object" && "type" in response) {
    if (response.type === "text") {
      // May not narrow - still typed as ApiResponse
      console.log(response.content); // Error
    }
  }
}

// CORRECT - use specific type or type guard
function isMyData(value: ApiResponse): value is MyData {
  return (
    typeof value === "object" &&
    value !== null &&
    "type" in value &&
    (value.type === "text" || value.type === "image")
  );
}

function processCorrect(response: ApiResponse) {
  if (isMyData(response)) {
    // Now response is MyData
    if (response.type === "text") {
      console.log(response.content); // OK
    }
  }
}

Use type guards to narrow to your discriminated union first, then narrow within it.

5Ensure discriminant property is required in all members

If the discriminant property is optional in some union members, narrowing fails:

typescript
// WRONG - discriminant is optional
type BadState =
  | { status?: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; error: string };

function render(state: BadState) {
  if (state.status === "success") {
    // Cannot narrow reliably - status might be undefined
    console.log(state.data); // Error
  }
}

// CORRECT - discriminant is required in all members
type GoodState =
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; error: string };

function renderGood(state: GoodState) {
  if (state.status === "success") {
    console.log(state.data); // OK
  }
}

Make the discriminant property required (non-optional) in every union member.

6Use const assertions for literal values

When creating discriminated union values, use as const to ensure literal types:

typescript
type Result =
  | { type: "success"; value: number }
  | { type: "failure"; error: string };

// WRONG - type is inferred as string, not literal "success"
const result1 = { type: "success", value: 42 };
// Type: { type: string; value: number }

// This won't narrow correctly
function handle(r: Result) {
  if (r.type === "success") {
    console.log(r.value); // May error
  }
}

// CORRECT - use const assertion
const result2 = { type: "success", value: 42 } as const;
// Type: { readonly type: "success"; readonly value: 42 }

// Or explicitly type it
const result3: Result = { type: "success", value: 42 };
// Type: Result (which narrows correctly)

Use as const or explicit type annotations when creating discriminated union values.

Advanced notes

### TypeScript Version Differences

Discriminated union narrowing has improved significantly across TypeScript versions. Earlier versions (pre-3.5) had more limitations. If you're experiencing issues:

- Upgrade to the latest TypeScript version (5.x+ recommended)
- Check the [TypeScript release notes](https://www.typescriptlang.org/docs/handbook/release-notes/overview.html) for narrowing improvements
- Some edge cases may be known bugs tracked in the TypeScript GitHub issues

### Complex Generic Discriminated Unions

When using generics with discriminated unions, ensure the discriminant survives type parameter substitution:

typescript
type Result<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: Error };

function unwrap<T>(result: Result<T>): T {
  if (result.status === "success") {
    return result.data; // Correctly narrowed
  }
  throw result.error;
}

### Exhaustiveness Checking

Use the never type to ensure all discriminated union cases are handled:

typescript
type Action =
  | { type: "increment" }
  | { type: "decrement" }
  | { type: "reset" };

function reducer(action: Action) {
  switch (action.type) {
    case "increment":
      return;
    case "decrement":
      return;
    case "reset":
      return;
    default:
      // If you add a new Action type and forget to handle it,
      // this line will error because action won't be 'never'
      const _exhaustive: never = action;
      return _exhaustive;
  }
}

### Union Distribution in Conditional Types

Discriminated unions interact with conditional types through distribution. This can affect narrowing in mapped types:

typescript
type ExtractByType<T, K extends string> = T extends { type: K } ? T : never;

type Action =
  | { type: "add"; payload: number }
  | { type: "remove"; payload: number };

// Extracts only the "add" action type
type AddAction = ExtractByType<Action, "add">;
// Result: { type: "add"; payload: number }

### Known Limitations

Some patterns inherently prevent discriminant narrowing and are tracked as TypeScript issues:

- Issue #48522: Non-literal discriminator properties don't narrow
- Issue #56106: Narrowing fails when types are assignable to each other
- Issue #55425: Narrowing breaks when discriminating from wider union types
- Issue #31404: Union discriminant properties with union values
- Issue #9919: Intersection with discriminated unions breaks narrowing

For these cases, consider using explicit type guards or restructuring your types to avoid the problematic pattern.

Frequently asked questions

What does "Discriminant property 'X' cannot be used for narrowing" mean?
TypeScript's discriminated unions are a powerful pattern for working with union types that share a common literal property (the discriminant). When you check the discriminant property in a conditional, TypeScript can narrow the union to the specific type that matches the discriminant value. However, TypeScript cannot perform this narrowing when: 1.
How do I fix "Discriminant property 'X' cannot be used for narrowing"?
This TypeScript error occurs when a discriminant property in a union type cannot effectively narrow the type due to issues like non-literal types, assignable types overlapping, or intersection conflicts. Proper discriminated unions require literal type discriminants and non-overlapping type members.
What causes "Discriminant property 'X' cannot be used for narrowing"?
Discriminant property uses non-literal types (string, number, boolean instead of 'success', 42, true) One type in the union is structurally assignable to another (overlapping shapes) Discriminated union is wrapped in an intersection type that breaks narrowing
Is the "Discriminant property 'X' cannot be used for narrowing" error serious?
It is a medium-severity error — more of a blocker than a risk. The steps above resolve it.

Official resources & further reading