type PlayerOne = { kind: "playerOne"; name: string; }; type PlayerTwo = { kind: "playerTwo"; name: string; }; type Player = PlayerOne | PlayerTwo; type Point = "love" | "15" | "30"; type Deuce = { kind: "deuce"; }; type PointsData = { kind: "points"; playerOnePoint: Point; playerTwoPoint: Point; }; type FortyData = { kind: "forty"; player: Player; otherPlayerPoint: Point; }; type Advantage = { kind: "advantage", player: Player }; type Game = { kind: "game", player: Player // otherPlayerPoint: ? Not representable here. }; type Score = Deuce | PointsData | FortyData | Advantage | Game; // Add the ignored _score parameter to ensure a type check. function scoreWhenDeuce(_score: Deuce, player: Player): Advantage { return { kind: "advantage", player }; } function scoreWhenAdvantage(score: Advantage, player: Player): Game | Deuce { return score.player.kind === player.kind ? { kind: "game", player: score.player // == player. } : { kind: "deuce" }; } function incrementPoint(point: Point): "15" | "30" | undefined { switch (point) { case "love": return "15"; case "15": return "30"; case "30": return undefined; } } function scoreWhenForty(score: FortyData, player: Player): Game | Deuce | FortyData { if (score.player.kind === player.kind) { return { kind: "game", player: player }; } else { // "15" | "30" | undefined is a subset of Point | undefined const newPoints: Point | undefined = incrementPoint(score.otherPlayerPoint); return (newPoints === undefined) ? { kind: "deuce" } : {...score, otherPlayerPoint: newPoints}; } } function updatePointsData(score: PointsData, newPoints: Point, player: Player): PointsData { // Discriminated union, with player.kind being the discriminant, player the // union, and the switch the typeGuard. switch (player.kind) { case "playerOne": return {...score, playerOnePoint: newPoints}; case "playerTwo": return {...score, playerTwoPoint: newPoints}; } } function createFortyData(score: PointsData, player: Player): FortyData { let otherPlayerPoint = player.kind === "playerOne" ? score.playerTwoPoint : score.playerOnePoint; return { kind: "forty", player, otherPlayerPoint }; } function scoreWhenPoints(score: PointsData, player: Player): PointsData | FortyData { const newPoints: Point | undefined = player.kind === "playerOne" ? incrementPoint(score.playerOnePoint) : incrementPoint(score.playerTwoPoint); return newPoints === undefined ? createFortyData(score, player) : updatePointsData(score, newPoints, player); } function score(score: Score, player: Player): Score { // XXX: refactor to avoid the switch and just use a method map. switch (score.kind) { case "points": return scoreWhenPoints(score, player); case "forty": return scoreWhenForty(score, player); case "deuce": return scoreWhenDeuce(score, player); case "advantage": return scoreWhenAdvantage(score, player); case "game": return score; } }