facebook / flow

Adds static typing to JavaScript to improve developer productivity and code quality.
https://flow.org/
MIT License
22.08k stars 1.85k forks source link

String literal types and union type guards toward tagged unions #135

Closed Nevor closed 9 years ago

Nevor commented 9 years ago

As OCaml programmers, you know that tagged unions are very useful a wide range of situation and it might be interesting to encode them in Javascript in a safe way (i.e. type checked by Flow).

Current workaround and motivating example

One way of encoding them is to create a union type of disjoint types, the usual way is to use a tag property that will defer for each "constructor". You already encode them that way in your flux-chat example :

Definitions :

type ServerReceiveRawMessagesAction = {
  type: any;
  rawMessages: Array<RawMessage>;
};

type ServerReceiveRawCreatedMessageAction = {
  type: any;
  rawMessage: RawMessage;
};

type ServerAction = ServerReceiveRawMessagesAction
                  | ServerReceiveRawCreatedMessageAction;

[...]

Destruction :

MessageStore.dispatchToken = ChatAppDispatcher.register(function(payload: any) {
  var action = payload.action;

  switch(action.type) {
[...]

    case ActionTypes.RECEIVE_RAW_MESSAGES:
      _addMessages(action.rawMessages);
      [...]
      break;

    default:
      // do nothing
  }

});

Unfortunately, in these example, you are using the any type for tag and payload. The construction and destruction is not safe, anything is usable as a tag and anything can go through a switch case.

// bad construction

var invalidButAccepted : ServerReceiveRawMessagesAction = {
  type : "foobar",
  rawMessages : []
};

// or if we pass a bad type to the switch case

var action = { type : ActionTypes.RECEIVE_RAW_MESSAGES; foobar : 3 };
// will break at _addMessages(action.rawMessages);

A quick safe solution is to use some kind of singleton type as a tag (for instance a string literal type) and to extend type guards to the switch statement and to narrow union types.

Previous proposal in Typescript

This very situation is also a problem we spotted in Typescript too. Leveraging the recent addition of type aliases and union type, we have made a same proposal to Typescript developers. https://github.com/Microsoft/TypeScript/issues/1003. Furthermore, we have developed a prototype Typescript extended to handle string literal types in implementation files and extended if and swtich guards.

In our implementation, your chat example would roughly become :

type ServerReceiveRawMessagesAction = {
  type: "RawMessages";
  rawMessages: Array<RawMessage>;
};

type ServerReceiveRawCreatedMessageAction = {
  type: "RawCreatedMessage";
  rawMessage: RawMessage;
};

type ServerAction = ServerReceiveRawMessagesAction
                  | ServerReceiveRawCreatedMessageAction;

[...]
MessageStore.dispatchToken = ChatAppDispatcher.register(function(payload: Payload<...>) {
  var action = payload.action; // action of right type

  switch(action.type) { // valid because type is common to all types in union
[...]

    case ActionTypes.RECEIVE_RAW_MESSAGES: // assuming this of type "RawMessages"
      _addMessages(action.rawMessages); // action is narrowed to the right type and checked okay
      [...]
      break;

    // case "foo" : would be an error 

    default:
      // we could check that the switch is exhaustive
      // do nothing
  }

});

The construction and destruction of the tagged union are then type checked for a safer usage.

Any plan in Flow

The string literal types (albeit not documented) and simple type guards are already available and working as expected in Flow. The question is whether you are planning to go toward this direction in term extended type guards or safe tagged union (in any chosen encoding).

As a side note, we are willing to contribute if it's acceptable but not planned.

avikchaudhuri commented 9 years ago

I gave a talk yesterday pointing this very example, among others, as future work. :)

Raynos commented 9 years ago

@avikchaudhuri do you have a link to said talk ?

samwgoldman commented 9 years ago

Is this any different from #20? If not, can we close this in favor of that issue to keep the conversation in one place?

samwgoldman commented 9 years ago

Merging with #20.