Code Review Videos > Broken Link Checker > TypeScript > Validating The URL

Validating The URL

Getting the fetcher code up and running seemed like a fairly big job.

After a big job, I usually take on something smaller and easier, aiming for that quick win.

Looking at the Proof of Concept code a quick win would be validating the URL.

Here’s the code:

  try {
    if (redirected) {
      const hrefToUrl = new URL(location);
      console.log(`hr`, hrefToUrl);
    }
  } catch (e) {}Code language: TypeScript (typescript)

I mean, it’s bad.

Swallowing exceptions and silently doing nothing is rarely a smart move. But hey, proof of concept, right?

Yeah, it’s a poor excuse.

Anyway, let’s make this better.

URL Validator Unit Tests

JavaScript provides us with the URL object.

Assuming we have a valid URL as a string, we can use this when creating a new URL(...);.

This object then gives us a bunch of properties and methods for working with URLs in a nice way. We can get the hostname, or the protocol, or the path. Also we could set up a query string (?hello=world), or mess around with the fragment (#things).

But, if you don’t provide a valid URL when instantiating a new URL, then you get an error:

So we can use this to our advantage.

Providing we wrap the call to create a new instance of a URL object in a try / catch block, then we can return a boolean to say whether the given URL was valid or not.

Here are some unit tests to cover the various cases I can think of:

import { isValidUrl } from "./is-valid-url";

describe("isValidUrl", () => {
  [
    [null, false],
    ["", false],
    ["null", false],
    ["http://", false],
    ["https://", false],
    ["https://example.com", true],
    ["https://www.example.com", true],
    [
      "https://learn.microsoft.com/dotnet/core/tutorials/top-level-templates",
      true,
    ],
  ].forEach(([given, expected]) => {
    test(`given: ${given}, expected: ${expected}`, () => {
      expect(isValidUrl(given as string | null)).toEqual(expected);
    });
  });
});
Code language: TypeScript (typescript)

We’re throwing a variety of weirdness at this.

The structure I’m using here is [input, expectedOutcome].

Taking the first entry in our array we therefore have:

  • input: null
  • expected outcome: false

If the given value is null then no, this is not going to be a valid URL.

Likewise, an empty string on line 6 is also invalid.

Things might look initially correct, such as on lines 8 & 9, but they don’t make a full URL, so are also invalid.

Everything from line 10 onwards should be valid.

A Basic isValidUrl Implementation

The code here is really straightforward:

export const isValidUrl = (url: string | null) => {
  if (!url) return false;
  try {
    new URL(url);
    return true;
  } catch (e) {
    return false;
  }
};Code language: TypeScript (typescript)

We have to allow for null as it’s possible that there will be no “next location” in our wider implementation.

Typing given

The one quirk to this otherwise fairly standard JavaScript-y looking TypeScript code is the type assertion against given on line 18:

expect(isValidUrl(given as string | null)).toEqual(expected);Code language: TypeScript (typescript)

And if we narrow this down further:

isValidUrl(given as string | null)Code language: TypeScript (typescript)

If we don’t provide any type information then TypeScript will get confused:

And you may be wondering why the error mentions that given could be a boolean when we only seem to have string and null values here.

Being completely honest, that is something I didn’t even notice until I got the error.

The issue here, to the very best of my knowledge, is that TypeScript has been unable to infer that my outer array contains several tuples. It would appear that TypeScript has only been able to infer that my individual test cases are arrays of string and boolean and null values.

There are a couple of ways to fix this.

Type The Tuple

One way is to provide a more explicit type to the array:

  (
    [
      [null, false],
      ["", false],
      ["null", false],
      ["http://", false],
      ["https://", false],
      ["https://example.com", true],
      ["https://www.example.com", true],
      [
        "https://learn.microsoft.com/dotnet/core/tutorials/top-level-templates",
        true,
      ],
    ] as [null | string, boolean][]
  ).forEach(([given, expected]) => {
    test(`given: ${given}, expected: ${expected}`, () => {
      expect(isValidUrl(given)).toEqual(expected);
    });
  });
Code language: TypeScript (typescript)

Here we describe that the type of data in our array will have the shape of null | string for the first element, and the second element will always be a boolean.

Now given is happily narrowed to either null | string, and expected will only ever be a boolean.

Use A Type Cast

Another way is to provide an explicit type cast (or type assertion) against given:

expect(isValidUrl(given as string | null)).toEqual(expected);Code language: JavaScript (javascript)

That’s the implementation I used above.

This is significantly looser.

Here, expected could still be of type string | boolean | null, but Jest doesn’t care. So long as the outcome matches the expected value, all is fine.

However, we have kinda glossed over that really we don’t quite know what given may be when we run this. But we’re fairly sure it will be one of the two types we provided.

That said, this won’t magically change the run time value of given.

If given were some other value, such as a boolean or another array, or whatever, this wouldn’t be magically changed at run time. That might blow things up. In our specific case, it’s probably fine.

Not great.

However, both approaches solve the problem.

My Preference

Where possible, I prefer the stricter variant.

That means, for me, I prefer the typed Tuple approach.

I think both are a little tricky to read in their own ways.

One way to slightly improve things for the typed tuple approach might be to name the type:

Ultimately just because this is test code doesn’t mean we should give it any less love than we would for implementation code. Heck, I usually spend longer in the tests than on the code itself.

But as ever, personal preference.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.