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
- 787