Rick Carlino

Personal blog of Rick Carlino, senior software engineer at Qualia Labs, co-founder of Fox.Build Makerspace. Former co-founder of FarmBot.

Stack Overflow Reddit Linkedin Github Stack Overflow RSS

Real World Use Cases for the Typescript Unknown Type

Why

Typescript offers many features over vanilla Javascript. The main selling point of the language has always been type safety. When people migrate their Javascript code bases over to Typescript, it is almost always with the intent of reducing runtime errors.

Typescript's strictness makes it less likely that you will write unsafe code, but there are still ways that a developer can forfeit runtime safety.

The any Type

The most common source of runtime errors in Typescript is the any type, which essentially turns off the type checker. If your Typescript codebase sees null pointer exceptions or errors such as undefined is not a function, chances are, there is an any type floating around the codebase somewhere.

The any type should be avoided at all costs, as it negates the benefits of structural typing. Despite this, there are a number of reasons that any appears in a codebase (they are all unfortunate):

  • You are migrating a legacy application to TypeScript and it would not be feasible to declare types for all pre-existing data structures.
  • You use a third party library that has poor type or missing type coverage.
  • You are handling data that cannot be inspected until runtime (example: passing unsafe strings to JSON.parse(), I/O operations, untyped event emitters, etc..)

If you wonder why Typescript allows the any type at all, it is important to remember that Typescript is a 100% interoperable superset of Javascript. It would not be possible to completely remove the any type while also staying compatible with the underlying ecosystem and runtime.

A new type was added to Typescript 3.0 and it can help developers deal with realworld tradeoffs such as the ones listed above. Spoiler alert: it's the one I mentioned in the title.

The unknown Type

Typescript 3.0 shipped with a new unknown type, which is a stricter, more paranoid version of the any type. Like the any type, it can be used in places where runtime structure is not known. Unlike the any type, it will not allow you to accidentally call non-existent properties on objects (or worse, call a function that is not a function at all).

Here's an unsafe example of any:

function inspectFooBar(x: any) {
  // *runtime* type error:
  //     `TypeError: undefined is not a function`
  console.log(x.foo.bar());
}

In the example above, the Typescript compiler is trusting us to ensure that the x parameter has a callable foo.bar() property. Keeping track of that fact in a large codebase can be challenging (or impossible).

The code below shows a safer, more paranoid version that uses the uknown type. Although both of these examples are invalid, the example below is preferable because the error happens at compile time rather than runtime:

function inspectFooBar(x: unknown) {
  // Object is of type 'unknown'.
  console.log(x.foo.bar());
}

Although the example above is safer, it would appear that the unknown type is useless- what point is there in having a type with inaccessible properties? This is not the case. When used in conjunction with user defined type guards, the unknown type is a useful tool for safe programming. The following section requires a working knowledge of user defined type guards, which are explained in the Typescript Documentation.

Combining unknown with Type Guards

A common pattern

// Helper interface that describes an object with
// unknown attributes.
type ObjectLike = { [key: string]: unknown };

const isUnknownObject = (
  x: unknown,
): x is ObjectLike => (x !== null && typeof x === "object");

interface User {
  firstName: string;
  lastName: string;
}

function isUser(user: unknown): user is User {
  if (isUnknownObject(user)) {
    const hasFirst = typeof user.first === "string";
    const hasLast = typeof user.last === "string";
    return hasFirst && hasLast;
  }
  return false;
}

Using unknown in Type Guards

interface User {
  name: {
    first: string;
    last: string;
  };
}

function isUser(u: any): u is User {
  if (u && u.name) {
    const hasFirst = (u as User).name.first !== undefined;
    const hasLast = (u as User).name.last !== undefined;
    return hasFirst && hasLast;
  }

  return false;
}

The example above is simple and would be unlikely to cause confusion. As the application changes, or as the User interface begins to take no more responsibilities, the function has the propensity to cause problems. Most notably:

  • It uses the any type, which inevitably leads to misfortune.
  • It assumes that you "Know What You're Doing™️", even as the interface evolves. Example: You decide to make some fields optional (or rename them) six months later.
  • It requires two typecasts. Although typecasts are not as bad as the any type, they still can lead to runtime errors.

Other Safety Precautions

  • Refactor jumbled data structures into TaggedUnions
  • Set "strict": true in tsconfig.json

(Don't add this, maybe?) Avoiding The object Type

interface ObjectLike {
  [key: string]: unknown;
}

If you enjoyed this article, please consider sharing it on sites like Hacker News or Lobsters.