I have UNO card deck that I want to be type safe.
Each card in the deck array is represented by a 2 char string consting of a colour and a value. I am using a Template Literal Type
for the CardType
.
type SpecialCard = "S" | "R" | "D"; // Skip | Reverse | Draw 2
type NumberCard = number;
type Color = "R" | "Y" | "G" | "B";
type WildCard = "W" | "W4";
type CardType = `${Color}${(NumberCard | SpecialCard)}` | WildCard;
Now I'd like to create a set of cards like this:
const StandardDeck: CardType[] = (["R", "Y", "G", "B"] as Color[])
.map<CardType[]>((color) =>
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "S", "R", "D"].map<CardType>(
(v) => `${color}${v}`
)
).flat()
However this gives me an error on this line:
(v) => `${color}${v}`
Type '`R${number}` | `Y${number}` | `G${number}` | `B${number}` | `R${string}` | `Y${string}` | `G${string}` | `B${string}`' is not assignable to type 'CardType'.
Type '`R${string}`' is not assignable to type 'CardType'.
Type 'string' is not assignable to type 'CardType'.
Type '`R${string}`' is not assignable to type '"BD"'.
Type 'string' is not assignable to type '"BD"'.
CodePudding user response:
type SpecialCard = "S" | "R" | "D"; // Skip | Reverse | Draw 2
type NumberCard = number;
type Color = "R" | "Y" | "G" | "B";
type WildCard = "W" | "W4";
type Indexes = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
type CardType = `${Color}${(NumberCard | SpecialCard)}` | WildCard;
const applyColor = <Colors extends Color[]>(colors: [...Colors]) =>
<ColorKeys extends Indexes | SpecialCard, ColorMap extends ColorKeys[]>(colorMap: [...ColorMap]) =>
colors
.map((color) =>
colorMap.map((v): `${Colors[number]}${ColorKeys}` => `${color}${v}`)
).flat()
const makeDeck = applyColor(["R", "Y", "G", "B"])
// ("R0" | "R4" | "R1" | "R2" | "R3" | "R5" | "R6" | "R7" | "R8" | "R9" | "RR" | "RS"
// | "RD" | "Y0" | "Y4" | "Y1" | "Y2" | "Y3" | "Y5" | "Y6" | "Y7" | "Y8" | "Y9" | "YR" | "YS" | "YD" | "G0" | ... 24 more ... | "BD")[]
// 28 24 = 52 elements in the union
// exactly same number of elements is in runtime value
const StandardDeck = makeDeck([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, "S", "R", "D"])
Explanation
I have used curry
pattern to make it more readable and variadic tuple types for inference
As you might have noticed, in order to infer types I have used a function instead of just a value.
If you want to type something complicated, first of all try to infer each function argument. The more strict inference - the better.
Hover your mouse on applyColor(["R", "Y", "G", "B"])
. You will see that array is infered with each element, not just string[]
. Same with makeDeck
.
It is easy to infer a tuple of primitives. You just need to create generic for a tuple Colors
and apply appropriate constraint extends Color[]
.
In the second function, apart from infering whole tuple, I also infered a tuple element ColorKeys
. I did it because I have used it in (v) =>
${color}${v}`` for typing the return type.
TypeScript is smart enough to infer exact type of ${color}${v}
.
If you are interested in function argument inference you can read my article. It is not finished yet but it already has some useful explanation and examples.
P.S. It seems to be that WildCard
does not exist in a runtime.