Skip to content

Impossible to transform from a discriminated union to another using a different discriminant field #35140

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

Closed
OzTK opened this issue Nov 16, 2019 · 2 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@OzTK
Copy link

OzTK commented Nov 16, 2019

It does not seem to be possible to change the discriminant from one property to another and have that recognised as the proper type.

TypeScript Version: 3.8.0-dev.20191116

Search Terms: discriminated union, discriminant, kind, type

Code

interface Car {
  kind: 'car'
  trunkSize: number
}

interface Bike {
  kind: 'bike'
  handlebarColor: string
}

interface ApiCar {
  type: 'car'
  trunkSize: number
}

interface ApiBike {
  type: 'bike'
  handlebarColor: string
}

type Vehicle = Car | Bike

type ApiVehicle = ApiCar | ApiBike

const unknown = {
  type: 'car',
  trunkSize: 5
}

function makeVehicle(vehicle: ApiVehicle): Vehicle {
  return { ...vehicle, kind: vehicle.type }
}

Expected behavior:
Object returned by makeVehicle should successfully be interepreted by the compiler as a Vehicle

Actual behavior:
Does not compile with the following error:

Type '{ kind: "car" | "bike"; type: "car"; trunkSize: number; } | { kind: "car" | "bike"; type: "bike"; handlebarColor: string; }' is not assignable to type 'Vehicle'.
  Type '{ kind: "car" | "bike"; type: "car"; trunkSize: number; }' is not assignable to type 'Vehicle'.
    Type '{ kind: "car" | "bike"; type: "car"; trunkSize: number; }' is not assignable to type 'Car'.
      Types of property 'kind' are incompatible.
        Type '"car" | "bike"' is not assignable to type '"car"'.
          Type '"bike"' is not assignable to type '"car"'.(2322)

Playground Link:
http://www.typescriptlang.org/play/?ts=3.8.0-dev.20191115&ssl=31&ssc=36&pln=30&pc=53#code/JYOwLgpgTgZghgYwgAgMJysg3gKGcga1ABMAuZAcgQwr2TCgFcQCBlYALwnJEYFsARtBwBfHDlCRYiFACFgBFLnxEQZSgIURa+ABZw1AGwgCMqAPaHzUcgGcGoAOajxk6PCTIAggAdg6TGV6AE8fbkpqKB16JhZ2Lh5+ISgXCXB3GW8-eUVsOjBQ8IpNRWj9IxMzS2s7BxBnMRwCsOQANQhdYARjZABeNAxkAB8AApyIcWaUX2B2zu6UfpmA4azgcfEEcxB7ZGYCEHMAdxA+vPwp8ioaABp82LZOcIBWVJhmBDBgbeQ+OEU5l1jAAKABuHSB4RmgIWAEpyDCekEoBAwIwoKcsMgAHS48HzYw3Qgkcj4yHYqbIMQiIA

@OzTK OzTK changed the title Impossible to transform from a discriminated union to another using a different discriminant Impossible to transform from a discriminated union to another using a different discriminant field Nov 16, 2019
@andrewbranch
Copy link
Member

Yeah, this is admittedly a weird problem, but it’s a known design limitation. I was looking for similar reports and the team pointed me to #31445, though I feel like I’ve seen simpler cases. The problem is that when you do { ...vehicle, kind: vehicle.type }, the type system is essentially tracking two types:

  1. vehicle, which is ApiCar | ApiBike, and
  2. vehicle.type, which is 'car' | 'bike'

but the type system can’t tell that these two types are dependent upon each other because they both originate from vehicle—doing so appears intuitive and easy here, but in practice it would get extraordinarily complex and expensive to track these relationships. So TypeScript sees this as trying to combine “either an ApiCar or an ApiBike” with a kind of “either 'car' or 'bike',” which is unsafe. E.g., TS thinks there’s a possibility that { ...vehicle could be a car but vehicle.type could be 'bike'.

You can get a working solution if you discriminate on type first, because then the type system does know the narrowed type of vehicle (and of course the type of vehicle.type), but you end up writing code that you’d never write in vanilla JS:

function makeVehicle(vehicle: ApiVehicle): Vehicle {
  if (vehicle.type === "car") return { ...vehicle, kind: vehicle.type };
  return { ...vehicle, kind: vehicle.type };
}

Personally, I think this is an appropriate place for a type assertion:

function makeVehicle(vehicle: ApiVehicle): Vehicle {
  return { ...vehicle, kind: vehicle.type } as Vehicle;
}

“TypeScript can’t know this is sound, but I know this is sound.”

@andrewbranch andrewbranch added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Dec 10, 2019
@OzTK
Copy link
Author

OzTK commented Dec 11, 2019

Thank you for the detailed explanation @andrewbranch ! I don't think the type assertion is even needed, just copying the property from type to kind was enough for me to have the object inferred as the right type.
Closing as this is already an aknnowledged limitation

@OzTK OzTK closed this as completed Dec 11, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

2 participants