DEV Community

loading...
Cover image for Fizz-buzz in TypeScript

Fizz-buzz in TypeScript

David Wickes
British. Strong opinions held weekly. No, that's not a typo. Teaches when and where and what I can.
・5 min read

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:

  1. If a number is divisible by three, say "Fizz!" instead
  2. If a number is divisible by five, say "Buzz!" instead
  3. If a number is divisible by three and by five, say "Fizz-Buzz!"
  4. 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]
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

and hovering the cursor over TEST. You should see the interpreted expression displayed.

test result showing that TEST equals N2

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
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

Again, we can manually test this:

type TEST = Equal<N1, N1>
Enter fullscreen mode Exit fullscreen mode

image showing that the expression evaluates to TRUE

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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]
Enter fullscreen mode Exit fullscreen mode

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'
Enter fullscreen mode Exit fullscreen mode

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>>
Enter fullscreen mode Exit fullscreen mode

and can be tested (and used) as we have seen above:

type TEST = FB<N15>
Enter fullscreen mode Exit fullscreen mode

Alt Text


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)
Enter fullscreen mode Exit fullscreen mode

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...


  1. 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. 

  2. 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. 

  3. 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. 

Discussion (3)

Collapse
bpedroza profile image
Bryan

If there is an award for most over-engineered fizz-buzz implementation ever, you win. Why though? Seeing code like this is nightmare fuel.

Collapse
gypsydave5 profile image
David Wickes Author • Edited

If there is an award for most over-engineered fizz-buzz implementation ever, you win.

Sadly this prize has already been awarded.

Why though? Seeing code like this is nightmare fuel.

Then my work has not been entirely in vain.

Collapse
bpedroza profile image
Bryan • Edited

Got me good. Perfect response :)