We have officially started the last two weeks of the year, I can already feel Mariah Carey taking over the background noise at the dinner table, ah let's get started it before I get more sentimental.
Let's keep the fun going, today we are going to be playing a little bit of Rock-Paper-Scissors!
Day sixteen - Dec 16
I strongly encourage you to try the first challenge on your own before reading the solution here.
Challenge: Rock, Paper, Scissors
The challenge
It's Sunday and there's one week to go before the big day (Christmas Eve) when the elfs' work for the year will finally be complete. For the last 20 years the only game the elves have had to play together is StarCraft. They're looking for a fresh game to play.
So, they get the idea to try a Rock, Paper, Scissors tournament.
But the elves are sorta nerdy so they want to accomplish this using TypeScript types. The WhoWins
should type to correctly determine the winner in a Rock-Paper-Scissors game. The first argument is the opponent and the second argument is you!
What's Rock, Paper, Scissors?
In case you haven't played it before, basically:
it's a two player game where each player picks one of three options: Rock (๐๐ป), Paper (๐๐พ), and Scissors (โ๐ฝ)
game rules:
Rock crushes Scissors (Rock wins)
Scissors cuts Paper (Scissors wins)
Paper covers Rock (Paper wins)
otherwise, a draw
The code to complete
type RockPaperScissors = '๐๐ป' | '๐๐พ' | 'โ๐ฝ';
type WhoWins = unknown;
The tests
import { Expect, Equal } from 'type-testing';
type test_0_actual = WhoWins<'๐๐ป', '๐๐พ'>;
type test_0_expected = 'win';
type test_0 = Expect<Equal<test_0_expected, test_0_actual>>;
type test_1_actual = WhoWins<'๐๐ป', 'โ๐ฝ'>;
type test_1_expected = 'lose';
type test_1 = Expect<Equal<test_1_expected, test_1_actual>>;
type test_2_actual = WhoWins<'๐๐ป', '๐๐ป'>;
type test_2_expected = 'draw';
type test_2 = Expect<Equal<test_2_expected, test_2_actual>>;
type test_3_actual = WhoWins<'๐๐พ', '๐๐ป'>;
type test_3_expected = 'lose';
type test_3 = Expect<Equal<test_3_expected, test_3_actual>>;
type test_4_actual = WhoWins<'๐๐พ', 'โ๐ฝ'>;
type test_4_expected = 'win';
type test_4 = Expect<Equal<test_4_expected, test_4_actual>>;
Note: The second param (think about it as "Player B") is the one that is being evaluated for whether it wins, loses, or results in a draw.
The brute force solution
There are a couple of ways of solving this problem, let's start with the more "naive" approach.
type RockPaperScissors = '๐๐ป' | '๐๐พ' | 'โ๐ฝ';
type WhoWins<A extends RockPaperScissors, B extends RockPaperScissors> =
A extends B ? 'draw' :
[A, B] extends ['๐๐ป', '๐๐พ'] ? 'win' :
[A, B] extends ['๐๐ป', 'โ๐ฝ'] ? 'lose' :
[A, B] extends ['๐๐พ', 'โ๐ฝ'] ? 'win' :
[A, B] extends ['๐๐พ', '๐๐ป'] ? 'lose' :
[A, B] extends ['โ๐ฝ', '๐๐ป'] ? 'win' :
[A, B] extends ['โ๐ฝ', '๐๐พ'] ? 'lose' : never;
The parameters A and B represent the players, which have to be a value of the RockPaperScissors
union.
Then we have a couple of conditional types, the first one:
A extends B ? 'draw' :
This translates to if both players choose the same move, the result is a draw.
Then we have a series of conditionals that represent all the possible moves. For the first move:
[A, B] extends ['๐๐ป', '๐๐พ'] ? 'win' :
If the first player chooses '๐๐ป' and the second player chooses '๐๐พ', the first player wins.
Then we have similar conditional checks for other combinations.
This solves the problem, but is far from DRY code, since we are hard coding all the different moves, we can definitely do better.
The optimized solution
type RockPaperScissors = '๐๐ป' | '๐๐พ' | 'โ๐ฝ';
type WinningMoves = {
'๐๐ป': 'โ๐ฝ';
'๐๐พ': '๐๐ป';
'โ๐ฝ': '๐๐พ';
};
type WhoWins<A extends RockPaperScissors, B extends RockPaperScissors> =
A extends B ? 'draw' :
B extends WinningMoves[A] ? 'lose' :
'win';
A really nice clean way of solving this problem is to leverage a dictionary of WinningMoves
:
type WinningMoves = {
'๐๐ป': 'โ๐ฝ';
'๐๐พ': '๐๐ป';
'โ๐ฝ': '๐๐พ';
};
This type represents a dictionary where each key (a move) has a corresponding value (the move it defeats).
Let's now break down what's happening in the WhoWins
type:
type WhoWins<A extends RockPaperScissors, B extends RockPaperScissors> =
A extends B ? 'draw' :
B extends WinningMoves[A] ? 'lose' : 'win';
Here the params are the same as the first solution, A and B = the players, representing a value of the RockPaperScissors
union.
Then we have two conditional types, the first one:
A extends B ? 'draw' :
This line is saying that If both players choose the same move, the result is a draw.
The second conditional type:
B extends WinningMoves[A] ? 'lose' : 'win';
Here we are stating that If the move chosen by the second player (B
) is a winning move against the move chosen by the first player (A
), then the first player loses.
Otherwise, the first player wins.
The WinningMoves
is doing a lot of the heavy lifting for this solution, resulting in a short and elegant answer.
In full transparency, it just feels good when you keep getting better at something and things start to become "easier".
Thanks for sticking around for another TypeScript challenge, one less challenge, and one day closer to Christmas ๐๐
๐ผ!
If you like this content consider checking out what I post on Twitter/X ๐ฆ