Discriminated Unions for UI States

Example Angular Usage and Overview of Compiler Implementation

 

@haskellandchill

@publicismedia

Use Case

You need to model a UI element where the User can not make a selection, select a specific object, or select "all objects".

 

Then you need to take an action based on what the User has done. What if you miss a case?

Demo

Types

interface NoneSelected {kind: "NoneSelected"};
interface AllSelected {kind: "AllSelected"};
interface ObjectSelected {kind: "ObjectSelected", object: Object};

type Selection = NoneSelected | AllSelected | ObjectSelected;

Values

let none: Selection = {kind: "NoneSelected"};

let all: Selection = {kind: "AllSelected"};

let object: Selection = {kind: "ObjectSelected", object: object};

Magic ✨

switch(this.selection.kind) {
  case "NoneSelected": return "maroon";
  case "AllSelected": return "teal";
  case "ObjectSelected": return this.selection.object.color;
  default: impossible(this.selection);
}

👀 🤔

function impossible(x: never): never { throw new Error(); };

Read Research Papers The Code!

The Idea

 

Create AST of switch syntax

 

Store switch flow constraints by binding over the AST

 

Check switch's flow constraints against its consumer's and components' flow constraints

AST

interface SwitchStatement extends Statement {
  kind: SyntaxKind.SwitchStatement;
  expression: Expression;
  caseBlock: CaseBlock;
}

interface CaseBlock extends Node {
  kind: SyntaxKind.CaseBlock;
  parent?: SwitchStatement;
  clauses: NodeArray<CaseOrDefaultClause>;
}

interface CaseClause extends Node {
  kind: SyntaxKind.CaseClause;
  parent?: CaseBlock;
  expression: Expression;
  statements: NodeArray<Statement>;
}

interface DefaultClause extends Node {
  kind: SyntaxKind.DefaultClause;
  parent?: CaseBlock;
  statements: NodeArray<Statement>;
}

History

The original implementation in the TypeScript compiler was syntax directed, following the shape of the AST. This means for example, that a node (which should be a type guard of some kind) in the AST can only narrow the type of its children.

Setting a type to a more specific type than its declared type is called narrowing or refining.

Refining types using type guards in TypeScript

Flow

The control flow graph can be implemented by giving each node of the AST an adjacency list containing the prior nodes in the control flow.

type FlowNode = ... | FlowSwitchClause | ...;
interface FlowSwitchClause extends FlowNodeBase {
  switchStatement: SwitchStatement;
  antecedent: FlowNode;
}

Binding

The compiler performs a bind step in a single pass over the AST setting the scopes of all variables, and reporting errors for unreachable code.

To avoid confusion, a node from the AST is called a node and a node from the control flow graph a flow node.

Development

Check It Out

function checkApplicableSignature(
  node: CallLikeExpression,
  args: ReadonlyArray<Expression>,
  signature: Signature
) {
  const thisType = getThisTypeOfSignature(signature);
  if (thisType && thisType !== voidType && node.kind !== SyntaxKind.NewExpression) {}
  const headMessage = Diagnostics.Argument_of_type_0_is_not_assignable_to_parameter_of_type_1;
  const argCount = getEffectiveArgumentCount(node, args, signature);
  for (let i = 0; i < argCount; i++) {}
}
"Argument of type '{0}' is not assignable to parameter of type '{1}'."

Ok I'll Stop Now 😅

Discriminated Unions for UI States

By Sandy Vanderbleek

Discriminated Unions for UI States

Talk given at TypeScriptNYC #2

  • 720