Ken Arnold
Ken Arnold

Reputation: 2013

How to extend a union type with an additional property?

I'm using disjoint union types to represent events, as is recommended for Redux-like actions. Generally this is working well, but in some parts of my app, events have an additional timestamp field. How do I annotate the type of a timestamped event without duplicating something?

I tried using intersection types to merge the additional required property, but the following fails:

/* @flow */

export type EvtA = {type: 'A', prop1: string};
export type EvtB = {type: 'B', prop2: string};

export type Event =
  | EvtA
  | EvtB;

type Timestamped = { timestamp: number };

type TSEvent = Event & Timestamped;
function show(event : TSEvent) {
  console.log(event.timestamp);
//  let event = ((e: any): Event);
  if (event.type === 'A') {
    console.log(event.prop1);
  }
}

Error (on http://flow.org/try):

 function show(event : TSEvent) {
                          ^ all branches are incompatible: Either property `prop1` is missing in `EvtB` [1] but exists in `EvtA` [2]. Or property `prop1` is missing in `Timestamped` [3] but exists in `EvtA` [2]. Or property `prop2` is missing in `EvtA` [1] but exists in `EvtB` [4]. Or property `prop2` is missing in `Timestamped` [3] but exists in `EvtB` [4].
References:
12: type TSEvent = Event & Timestamped;
                   ^ [1]
7:   | EvtA       ^ [2]
12: type TSEvent = Event & Timestamped;
                           ^ [3]
8:   | EvtB;
       ^ [4]
17:     console.log(event.prop1);
                    ^ Cannot get `event.prop1` because: Either property `prop1` is missing in `EvtB` [1]. Or property `prop1` is missing in `Timestamped` [2].
References:
12: type TSEvent = Event & Timestamped;
                   ^ [1]
12: type TSEvent = Event & Timestamped;
                           ^ [2]

The commented-out typecast is my current hacky workaround.

(Yes, perhaps the cleaner approach would have been type LogEntry = { event: Event, timestamp: number }, but that requires changing a lot of other code.)

Upvotes: 1

Views: 220

Answers (1)

James Kraus
James Kraus

Reputation: 3478

What you're probably looking for is an object spread:

(Try)

/* @flow */

export type EvtA = {type: 'A', prop1: string};
export type EvtB = {type: 'B', prop2: string};

export type Event =
  | EvtA
  | EvtB;

type Timestamped = {timestamp: number };

type TSEventA = {
  ...EvtA,
  ...Timestamped,
};

// TSEventA now has type:
// {prop1?: mixed, timestamp?: mixed, type?: mixed}

function show(event : TSEventA) {
  console.log(event.timestamp);
  //  let event = ((e: any): Event);
  if (event.type === 'A') {
    console.log(event.prop1);
  }
}

You can retain all the type information by spreading exact objects:

(Try)

/* @flow */

export type EvtA = {|type: 'A', prop1: string|};
export type EvtB = {|type: 'B', prop2: string|};

export type Event =
  | EvtA
  | EvtB;

type Timestamped = {|timestamp: number|};

type TSEvent = {
  ...Event,
  ...Timestamped
};

// TSEvent now has union type:
// {prop2: string, timestamp: number, type: "B"}
// | {prop1: string, timestamp: number, type: "A"}

function show(event : TSEvent) {
  console.log(event.timestamp);
  if (event.type === 'A') {
    console.log('A', event.prop1);
  } else if (event.type === 'B') {
    console.log('B', event.prop2);
  }
}

Upvotes: 1

Related Questions