Typescript 4.1 has released some awesome new features, including Template Literals - which enable some really powerful stuff!
To try it out, I've done the generic FizzBuzz test only using Typescript Types!
Prithpal-Sooriya / ts-fizz-buzz
The generic Fizz Buzz test built entirely with TypeScript type annotations.
ts-fizz-buzz
The generic Fizz Buzz test built entirely with TypeScript type annotations.
This means that is works solely on typescript types - so it is a compile time Fizz Buzz "Solution".
And using VSCode IntelliSense you see the solution without even "running" your code!
See a live demo
This post is a breakdown/explanation of the types created & used in the Repo.
Outline & Plan.
FizzBuzz is a basic programming task that is (was?) used in interviews.
For those who've never seen the Fizz Buzz problem here it is:
Write a program that prints the numbers from 1 to 100. But for multiples of three print “Fizz” instead of the number and for the multiples of five print “Buzz”. For numbers which are multiples of both three and five print “FizzBuzz”.
Before we delve into the solution, I think its worth breaking down the tasks we need to accomplish:
- Build our Predicate Types. We need a way to see if a number is divisible by 3 or 5.
- Combine our Predicate Types & solve FizzBuzz for a single number.
- Solve FizzBuzz for an array of numbers.
Predicate Types - check the divisibility
We want to check if a number is divisible by 3 or 5, but sadly TypeScript (currently) does not support arithmetic operations - e.g. we can't use a modulus to see if a number is divisible.
But TS Template Literals now allow us to build some of the divisibility rules!
// 5 Divisibility rule, we only need to look at last digit & see if it is a 0 or 5.
export type IsDivisibleBy5<T extends number> =
`${T}` extends `${infer OtherDigits}${0 | 5}` ? true : false;
Breakdown:
-
<T extends number>
- This enforces users to only input numbers. -
`${T}`
- The back ticks are the new template literal syntax, this allows us to convert the number input into a string. -
`${infer OtherDigits}...`
- the infer acts like a wildcard that is (in this case) a string without the final character. -
`${0 | 5} ? true : false
- checks if the last number is 0 or a 5. It if is, then the Type resolves totrue
otherwisefalse
.
Sadly I couldn't generate the number 3 divisibility rule (since it includes adding each digit, which requires an arithmetic operation), so stuck with a simple union of valid numbers.
// Unsure if it is possible to generate a 3 Divisibility rule using current version of Typescript
type Finite3Tuple = [
0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33,
36, 39, 42, 45, 48, 51, 54, 57, 60, 63, 66,
69, 72, 75, 78, 81, 84, 87, 90, 93, 96, 99
]
export type IsDivisibleBy3<T extends number> =
T extends Finite3Tuple[number] ? true : false;
Fizz Buzz for a single number
// This is to remove the number at end of recursive concatenation in FizzBuzz below.
// E.g. Fizz4, Fizz2, ...
type RemoveTrailingNumber<Str extends string | number, T extends number> =
`${Str}` extends `${infer Rest}${T}` ? Rest : Str
// Helper type to abstract the recursion & conversion
type ConcatBuzz<T extends number, BuzzPredicate> = RemoveTrailingNumber<FizzBuzz<T, null, BuzzPredicate>, T>
/* Fizz Buzz type used for single number */
export type FizzBuzz<
T extends number,
FizzPredicate = IsDivisibleBy3<T>,
BuzzPredicate = IsDivisibleBy5<T>
> =
FizzPredicate extends true ? `Fizz${ConcatBuzz<T, BuzzPredicate>}` :
BuzzPredicate extends true ? `Buzz` :
T
The idea behind it is that it will recursively build the FizzBuzz string. Whenever the Fizz
is found, we can then combine the Buzz
onto it (if found). To prevent the Fizz/Buzz from continuously being concatenated, we set their predicate to null
.
Lets focus on the Main FizzBuzz type, the other types are just helpers to abstract out the recursion & trimming the output.
- It takes 3 arguments/generics.
T extends number
which is the input; and the Fizz & Buzz Predicates (FizzPredicate
&BuzzPredicate
). -
If
FizzPredicate
is valid, then we start building our string`Fizz${ConcatBuzz<T, BuzzPredicate>}`
.- The
ConcatBuzz
is just a helper type to abstract away the recursion. It recallsFizzBuzz
, but sets theFizzPredicate
tonull
, so the remaining iterations go through the rest of the predicates.
- The
If
BuzzPredicate
is valid, then we just return
.Buzz
Finally, if none of the predicates are valid then we just return the number.
Fizz Buzz for an array
type CalculateFizzBuzz<T extends number> = FizzBuzz<T, IsDivisibleBy3<T>, IsDivisibleBy5<T>>
export type FizzBuzzArray<Arr extends number[]> = {
[K in keyof Arr]: CalculateFizzBuzz<Arr[K] & number>
}
CalculateFizzBuzz
is just a helper type that allows us to simplify FizzBuzzArray
type.
Breakdown of FizzBuzzArray
:
Arr extends number[]
- enforces that the type must take a number array. This will be our input.{ [K in keyof Arr]: ... }
- Arrays can be treated as objects where the keys are the indices. So we can use thekeyof
to go through each key/index.-
CalculateFizzBuzz<Arr[K] & number>
- This runs FizzBuzz for this value with the given index. The& number
is the TS intersection type - we can use it to enforce that the given value is a number.- Similarly, we could have used a ternary as well:
Arr[K] extends number ? CalculateFizzBuzz<Arr[K]> : never
- Similarly, we could have used a ternary as well:
Thats it!
Now if we give it an input array, we get the FizzBuzz result out of it!
Check out the TS playground in the Github repo for more (& for an extended version of FizzBuzz).
Top comments (0)