Home > Enterprise >  Typescript union type does not throw error
Typescript union type does not throw error

Time:12-29

So, type definitions:

// type definitions

class GenericDto {
  public ids: string[] = [];
  public dateFrom: string | null = null;
  public dateTo: string | null = null;
}

class FleetInformationDto extends GenericDto { }
class VehicleInformationDto extends GenericDto { }

enum ReportQueueAction {
  GENERATE
}

enum ReportQueueType {
  VEHICLE_INFORMATION,
  FLEET_INFORMATION
}

type ReportQueue = {
  action: ReportQueueAction;
  type: ReportQueueType.FLEET_INFORMATION;
  dto: FleetInformationDto
} | {
  action: ReportQueueAction,
  type: ReportQueueType.VEHICLE_INFORMATION,
  dto: VehicleInformationDto;
}

and implementation:

// implementation
const dto: FleetInformationDto = {
  ids: ["1", "2"],
  dateFrom: '2021-01-01',
  dateTo: '2021-02-01'
}

const queueData: ReportQueue = {
  action: ReportQueueAction.GENERATE,
  type: ReportQueueType.FLEET_INFORMATION,
  dto: dto
}

// ^ works as expected

but if we add "VehicleInformationDto" to type FLEET_INFORMATION it does not throw error

const dto2: VehicleInformationDto = {
  ids: ["1", "2"],
  dateFrom: '2021-01-01',
  dateTo: '2021-02-01'
}

const queueData2: ReportQueue = {
  action: ReportQueueAction.GENERATE,
  type: ReportQueueType.FLEET_INFORMATION,
  dto: dto2 // <-- no error thrown here
}

well, what's the catch here? am i missing something?

The question: Why am I able to assign VehicleInformationDto to dto inside queueData2 when typescript expects it to be FleetInformationDto?

Edit: OK, yeah, it's because they share the same properties, then, how could I add a check for that?

Playground

CodePudding user response:

Typescript is structurally typed, not nominally typed. This means that as far as Typescript is concerned these are the same type:

class FleetInformationDto extends GenericDto { }
class VehicleInformationDto extends GenericDto { }

While I think this is absolutely the correct choice for adding static typing to a language like Javascript where objects are a grab-bag of properties, it can lead to some subtle gotchas:

interface Vec2 {
  x: number
  y: number
}

interface Vec3 {
  x: number
  y: number
  z: number
}

const m = { x: 0, y: 0, z: "hello world" };
const n: Vec2 = m; // N.B. structurally m qualifies as Vec2!
function f(x: Vec2 | Vec3) {
  if (x.z) return x.z.toFixed(2); // This fails if z is not a number!
}
f(n); // compiler must allow this call

Here we're doing some graphics programming and have 2D and 3D vectors, but we have a problem: objects can have extra properties and still structurally qualify which leads to a problem in this union type (sound familiar?).

The answer in your particular case is to use a discriminant to easily distinguish the similar types in the union:

interface FleetInformationDto extends GenericDto {
    // N.B., fleet is a literal *type*, not a string literal
    // *value*.
    kind: 'fleet'
}

interface VehicleInformationDto extends GenericDto {
    kind: 'vehicle'
}

Here I've used strings, but any unique compile-time constant (any primitive value or members of an enum) will do. Also, since you're not instantiating your classes and using them purely as types I've made them interfaces but the same principles apply.

Playground

And now you can clearly see the error that type 'fleet' is not assignable to type 'vehicle'.

  • Related