Setting up a LeetCode testing environment in TypeScript

LeetCode is a platform for practicing technical interview question. Though it supports TypeScript, its built-in editor doesn't offer type validation, nor many of the features we know and love in modern code editors. In this post, we're going to build a TypeScript project that allows us to answer LeetCode questions locally using TypeScript and Jest.

If you want to jump ahead to the finished project, visit the repo here: https://github.com/sim8/leetcode-scratchpad

Step 1: Initialize empty project

Here we initialize an empty NPM project. Optionally you can also initialize a git repository here (which I'd recommend!)

npm init
# Follow setup instructions

Step 2: Install dependencies

Firstly we'll setup TypeScript by creating a tsconfig.json:

{
  "compilerOptions": {
    "strict": true
  }
}

Our next dependency to install is Jest. Jest is a JS testing framework, which will be the basis of us testing our LeetCode solutions locally. Here we install jest itself as well as the babel preset required for TypeScript support.

npm install --save-dev jest @babel/preset-typescript

Next, we initialise a Jest config file:

jest --init

Finally, we need to add @babel/preset-typescript to our babel.config.js:

module.exports = {
  presets: [
    ['@babel/preset-env', { targets: { node: 'current' } }],
    '@babel/preset-typescript',
  ],
};

Another optional (and recommended) step here is installing Prettier. This means code formatting will never trip you up, and is often a timesaver even with the smallest of projects.

Step 3: Building our test runner

Our next step is to build the code that will run our test cases for each LeetCode problem. Importantly, we'll build this in a way that allows us to specify test cases in the same files as our solution code, which will make things easier for us once we're solving the problems. Let's go!

// I'd typically put types in a top-level types.ts
// (particularly if I'm reusing them elsewhere).
// Putting here for display purposes :)

export type AlgorithmType = (...args: any[]) => any;

// Type the test cases
export type TestCase<Algorithm extends AlgorithmType> = {
  inputs: Parameters<Algorithm>;
  output: ReturnType<Algorithm>;
};

// Use a TS generic for the algorithm, so we get correct
// typing throughout
export function buildTests<Algorithm extends AlgorithmType>({
  algorithm,
  testCases,
}: {
  algorithm: Algorithm;
  testCases: TestCase<Algorithm>[];
}) {
  testCases.forEach((testCase) => {
    // Iterate each test case, printing out the inputs
    test(`Test case: ${testCase.inputs}`, () => {
      const result = algorithm(...testCase.inputs);
      expect(result).toEqual(testCase.output);
    });
  });
}

Step 4: Bringing it all together

All that's left to do is use our new test runner for solving a LeetCode problem:

import { buildTests } from '../utils/buildTests';

function singleNumber(nums: number[]): number {
  // Now for the important part, which is up to you! 😄
}

buildTests({
  algorithm: singleNumber,
  testCases: [
    // Here you can copy the example test cases from LeetCode. Doing
    // so provides you a quick feedback loop for solving.
    {
      inputs: [[2, 2, 1]],
      output: 1,
    },
    {
      inputs: [[4, 1, 2, 1, 2]],
      output: 4,
    },
  ],
});

We can then run npm test to run our tests, which prints our results:

PASS  problems/single-number.ts
  ✓ Test case: 2,2,1 (6 ms)
  ✓ Test case: 4,1,2,1,2 (1 ms)