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; otherPlayerPoints: Point; }; type Advantage = { kind: "advantage", player: Player }; type Game = { kind: "game", player: Player // otherPlayerPoints: ? Not representable here. }; type Score = Deuce | PointsData | FortyData | Advantage | Game; function scoreWhenDeuce(player: Player): Advantage { return { kind: "advantage", player, } } function scoreWhenAdvantage(score: Advantage, player: Player): Game | Deuce { return score.player.kind === player.kind ? { kind: "game", player, } : { kind: "deuce", }; } function incrementPoints(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, } } else { // "15" | "30" | undefined is a subset of Point | undefined const newPoints: Point | undefined = incrementPoints(score.otherPlayerPoints); return newPoints === undefined ? { kind: "deuce" } : { ...score, otherPlayerPoints: 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, otherPlayerPoints: otherPlayerPoint, } } function scoreWhenPoints(score: PointsData, player: Player): PointsData | FortyData { const newPoints: Point | undefined = player.kind === "playerOne" ? incrementPoints(score.playerOnePoint) : incrementPoints(score.playerTwoPoint); return (newPoints === undefined) ? createFortyData(score, player) : updatePointsData(score, newPoints, player); } function score(score: Score, player: Player): Score { switch (score.kind) { case "points": return scoreWhenPoints(score, player); case "forty": return scoreWhenForty(score, player); case "deuce": return scoreWhenDeuce(player); case "advantage": return scoreWhenAdvantage(score, player); case "game": return score; } }