Often as software developers we are asked to demonstrate basic competence with a programming language in order to secure employment. TypeScript's current popularity means it's very important that we are comfortable writing simple exercises in it. This post will show you how to write 'fizz-buzz' in pure TypeScript, without relying on any other languages or libraries.
What is 'fizz-buzz'
'Fizz-buzz' is a simple game you can play in company. Players take turns counting from one upwards, but must apply the following rules:
- If a number is divisible by three, say "Fizz!" instead
- If a number is divisible by five, say "Buzz!" instead
- If a number is divisible by three and by five, say "Fizz-Buzz!"
- Otherwise, just say the number as you would normally
This is often translated into an exercise where, when you provide your program with a number, it responds with correct thing to say were it playing a game of 'fizz buzz'.
Step One: Numbers
First of all we will need some numbers. Unfortunately, TypeScript does not come with any predefined number system so we will have to write our own, including some literals. Happily we only need the natural numbers, which are easily defined:
type N = Z | S<unknown>
type Z = 0
type S<N> = [S]
Using this we can quickly define enough numeric literals for our purposes:
type N1 = S<Z>
type N2 = S<N1>
// ...
type N14 = S<N13>
type N15 = S<N14>
We will need only one operation on these numbers, a way of subtracting one from a number:
type Sub1<N> = N extends S<infer P> ? P : Z
The other arithmetic operations (which we don't need for this example) are left as an exercise for the reader.
To test whether this is all working, we need to run our program through the TypeScript interpreter. The fastest way to do this is to write the following expression into VSCode:1
type TEST = Sub1<N3>
and hovering the cursor over TEST
. You should see the interpreted expression displayed.
Step Two: Truth
In order to test for properties of our numbers using checks like 'equal' or 'less than' we will need some algebra to express truth in. Happily we can use the built in values in this case:
type BOOL = true | false
This gives us enough to define Equal
and LessThan
recursively for our numbers2
type Equal<Na, Nb> = {
0: Nb extends Z ? true : false
1: {
0: false,
1: Na extends S<infer Pa> ? Nb extends S<infer Pb>
? Equal<Pa, Pb>
: never
: never
}[Nb extends Z ? 0 : 1]
}[Na extends Z ? 0 : 1]
type LessThan<Na, Nb> = {
0: false,
1: Na extends Z ? true
: Na extends S<infer Pa> ? Nb extends S<infer Pb>
? LessThan<Pa, Pb>
: never
: never
}[Nb extends Z ? 0 : 1]
Again, we can manually test this:
type TEST = Equal<N1, N1>
Step three: predicates
The two important predicates we need to implement fizz-buzz are IsMultipleOfThree
:
type IsMultipleOfThree<T> = {
1: true
0: {
0: false,
1: IsMultipleOfFive<Sub1<Sub1<Sub1<T>>>>
}[LessThan<T, N5> extends true ? 0 : 1]
}[Equal<T, N5> extends true ? 1 : 0]
and the very similar IsMultipleOfFive
:
type IsMultipleOfFive<T> = {
1: true
0: {
0: false,
1: IsMultipleOfFive<Sub1<Sub1<Sub1<Sub1<Sub1<T>>>>>>
}[LessThan<T, N5> extends true ? 0 : 1]
}[Equal<T, N5> extends true ? 1 : 0]
You may demonstrate that the above works by writing a test in a similar way to those above.
Refactor
A pattern is occurring repeatedly in our code, which we can extract out into its own operation:
type Ternary<B, P, Q> = {
1: P,
0: Q
}[B extends true ? 1 : 0]
We can now use it to increase the readability of some of our previous definitions:3
type IsMultipleOfThree<T> = {
1: true
0: Ternary<LessThan<T, N3>,
false,
T extends S<S<S<infer P>>>
? IsMultipleOfThree<P>
: never>
}[Equal<T, N3> extends true ? 1 : 0]
Step four: fizz-buzz
Now finally we can write our fizz-buzz program. We will need to define the possible outputs:
type FIZZ = 'fizz'
type BUZZ = 'buzz'
type FIZZBUZZ = 'fizzbuzz'
This, along with our previously defined Ternary
function, allows us to write the fizz-buzz program very succinctly and expressively:
type FB<N> = Ternary<IsMultipleOfThree<N>,
Ternary<IsMultipleOfFive<N>, FIZZBUZZ, FIZZ>,
Ternary<IsMultipleOfFive<N>, BUZZ, N>>
and can be tested (and used) as we have seen above:
type TEST = FB<N15>
Step five: going further
This simple program could be improved by adding some error messages and error handling. For instance, at present if we subtract one from zero we get zero, when really we should be seeing some sort of error. We should also think about ways in which we can handle these errors.
In addition, many fizz-buzz exercises require the operation to be applied to more than one number at once, held in some sort of list structure. Such a structure is, again, not present in TypeScript but can be quite quickly defined using methods similar to the above.
Final thoughts
Less experienced developers may be tempted to solve fizz-buzz by using JavaScript, the language which TypeScript parsitizes and also embeds within its syntax. For instance:
const fb = (n: number): number | string => (n % 3 === 0)
? ((n % 5 === 0) ? 'fizzbuzz' : 'fizz')
: ((n % 5 === 0) ? 'buzz' : n)
but obviously this code is just written in JavaScript, using TypeScript built in values as some sort of rudimentary type checker, and not in TypeScript, which is, as we all know, a dynamically typed and interpreted programming language.
This post is heavily inspired by this post by Kyle Kingsbury, which showed me the light.
Edit: changed the implementation because I could...
-
VSCode is by far the best TypeScript interpreter available, as it correctly evaluates our expressions. IntelliJ, in contrast, is buggy and cannot evaluate expressions that are even slightly recursive or nested. The ergonomics of these interpreters are all peculiar, it would be good if someone could write a simple TypeScript interpreter that wasn't embedded in an editor. ↩
-
Some of the peculiarities of the TypeScript syntax mean that we need to introduce a little indirection in order to write recursively. This is again unfortunate. ↩
-
Again, the peculiarities of TypeScript mean that we cannot entirely do away with the
{0:... 1:}[ ... ? 0 : 1]
syntax, as it gets huffy when the defined symbol is referenced directly in the same expression outside of a 'block', but it's still an improvement. ↩