DEV Community

Prithpal Sooriya
Prithpal Sooriya

Posted on • Edited on

FizzBuzz... but only using TypeScript Types

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!

GitHub logo 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!

IntelliSense Example

See a live demo

Playground Link




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:

  1. Build our Predicate Types. We need a way to see if a number is divisible by 3 or 5.
  2. Combine our Predicate Types & solve FizzBuzz for a single number.
  3. 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;
Enter fullscreen mode Exit fullscreen mode

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 to true otherwise false.

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

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

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 recalls FizzBuzz, but sets the FizzPredicate to null, so the remaining iterations go through the rest of the predicates.
  • 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>
}
Enter fullscreen mode Exit fullscreen mode

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 the keyof 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

Thats it!
Now if we give it an input array, we get the FizzBuzz result out of it!
Screenshot of TS FizzBuzz type

Check out the TS playground in the Github repo for more (& for an extended version of FizzBuzz).

Top comments (0)