🎉 I'm releasing 12 products in 12 months! If you love product, checkout my new blog workingoutloud.dev

Back to home

A Practical Look At The "Unknown" Type In Typescript

The unknown type in TypeScript has been a pain point for those entering into the domain of TypeScript. For those who do not understand the purpose of unknown when coming across it for the first time (myself included), then what has been introduced as a way to remove the type un-safety of any can instead lead to losing typing safety through misuse of casting without type narrowing.

In this post, we will look at a practical example of using the unknown type in TypeScript.

This post was inspired by some recent work at our company to remove any mis-casting or use of any in the codebase, and the reference-point for that PR was this article by Marius Schulz in 2019.

Source code can be found here

Prerequisites

  1. Basic familiarity with TypeScript.
  2. Basic familiarity with tsup.
  3. TypeScript set up on your local IDE is ideal.

Getting started

We will create the project directory demo-practical-unknown-in-typescript and set up some basics:

$ mkdir demo-practical-unknown-in-typescript $ cd demo-practical-unknown-in-typescript $ yarn init -y $ yarn add -D typescript tsup @tsconfig/recommended $ mkdir src $ touch src/index.ts tsconfig.json

In tsconfig.json, add the following:

{ "extends": "@tsconfig/recommended/tsconfig.json" }

Update package.json to look like this:

{ "name": "demo-practical-unknown-in-typescript", "version": "1.0.0", "main": "index.js", "author": "Your name here", "license": "MIT", "devDependencies": { "@tsconfig/recommended": "^1.0.1", "tsup": "^6.2.2", "typescript": "^4.7.4" }, "scripts": { "start": "node dist/index.js", "dev": "tsup src/index.ts --dts --watch --format esm,cjs", "build": "tsup src/index.ts --dts --format esm,cjs --minify" } }

Update src/index.ts to actively have a typing issue to check things are ready:

const fooStr: unknown = "foo"; fooStr.toUpperCase();

tsup won't type-check by default, but it will if we pass the --dts flag in.

Run yarn dev and you will see the following error:

src/index.ts(2,1): error TS2571: Object is of type 'unknown'.

At this point, we have a local project set up and can begin to work through some practical examples of unknown in projects.

Demystifying the unknown type

The unknown type is a type that represents any type but, but unlike any, it is not legal to do anything with an unknown type.

Therefore, it helps us introduce safety by narrowing the type down to a more specific type.

In the initial code that we added to src/index.ts, we get any error on attempting to use a string method on the unknown type.

We need to check that the type is a string before we can legally use the toUpperCase method. To remedy this, update the code to the following:

const fooStr: unknown = "foo"; if (typeof fooStr === "string") { fooStr.toUpperCase(); }

Now our code will compile! Although this first example is contrived, it is a good starting point to understand the unknown type.

A more practical example

A more common example of unknown making its way into your daily life as a developer is with a try/catch block.

The catch block by default in typescript will set the error as an unknown type, so we need to narrow the type down to a more specific type.

try { throw new Error("foo"); } catch (e) { console.log(e.message); }

Once again, if we check our logs, the compiler fails with the following:

src/index.ts(9,15): error TS2571: Object is of type 'unknown'.

You may have seen (or even yourself used) the following pattern to remedy this:

try { throw new Error("foo"); } catch (e) { console.log((e as Error).message); }

Casting will work, but it is not ideal as it removes the type safety that TypeScript provides.

If e was not actually an Error object, then we would be in a situation where we are trying to access a property on an object that may not exist.

Instead, we can use the instanceof operator to check that the error is an Error object:

try { throw new Error("foo"); } catch (e) { if (e instanceof Error) { console.log(e.message); } else { console.log("An error occured"); } }

Now our error is handled and we can be sure that the message property exists on the e object.

Being a little more practical

Another use case for the unknown type is arbitrary data that is returned from an API.

For example, we may have a function that returns a Promise that resolves to an unknown type. We can emulate this in our code simply by writing an object and declaring it as unknown:

const json: unknown = { foo: "bar", }; json.foo; // TypeError: Property 'foo' does not exist on type 'unknown'.

To get the type safety that we want, we can use type predicates to narrow in on the type.

const json: unknown = { foo: "bar", }; type Json = { foo: string; }; function isJsonType(json: unknown): json is Json { function hasValidShape( given: unknown ): given is Partial<Record<keyof Json, unknown>> { return typeof given === "object" && given !== null; } return hasValidShape(json) && typeof json.foo === "string"; } if (isJsonType(json)) { json.foo; // string }

Our isJsonType helper here is a type predicate that narrows the type down to Json if the json object has the correct shape and the foo property is a string.

But what happens for even larger object shapes? You could continue to extend the type predicate that we wrote above, or we could use a library like io-ts to decode the type for us and validate the type within our predicate function with minimal code.

Using io-ts to narrow down an unknown to a known type

We need to install io-ts and fp-ts for this example:

$ yarn add io-ts fp-ts

We can then update our src/index.ts file to look like this:

import * as t from "io-ts"; import { isRight } from "fp-ts/Either"; // ... omitted for brevity const moreJson: unknown = { foo: "bar", baz: { qux: "quux", }, bool: true, }; const moreJsonType = t.type({ foo: t.string, baz: t.type({ qux: t.string, }), bool: t.boolean, }); type ComplexJson = t.TypeOf<typeof moreJsonType>; function isComplexJsonType(json: unknown): json is ComplexJson { return isRight(moreJsonType.decode(json)); } if (isComplexJsonType(moreJson)) { moreJson.foo; // string moreJson.baz.qux; // string moreJson.bool; // boolean console.log("moreJson is ComplexJson"); } else { console.log("moreJson is not a ComplexJson"); }

We have created a new ComplexJson type that has more properties than our previous example.

We can then use ts-io to help create a way to decode our type and use the isRight function to validate that the decode value is a Right value (we won't touch the Either monad result stuff here, but if you are unfamiliar with functional programming then just know that isRight will help confirm if our type is decoded successfully and valid). Wrapping this up in a helper function isComplexJsonType allows us to use the type predicate to narrow down the type.

Throughout this tutorial, while yarn dev has been running, tsup has compiled our code and placed it in the dist folder. If we run node dist/index.js we will see the following output:

# Run the compiled code $ node dist/index.js moreJson is ComplexJson

Cool, so it looks like our moreJson object adheres to the ComplexJson type requirements.

If we update our code to comment out the bool property:

import * as t from "io-ts"; import { isRight } from "fp-ts/Either"; // ... omitted for brevity const moreJson: unknown = { foo: "bar", baz: { qux: "quux", }, // bool: true, }; const moreJsonType = t.type({ foo: t.string, baz: t.type({ qux: t.string, }), bool: t.boolean, }); type ComplexJson = t.TypeOf<typeof moreJsonType>; function isComplexJsonType(json: unknown): json is ComplexJson { return isRight(moreJsonType.decode(json)); } if (isComplexJsonType(moreJson)) { moreJson.foo; // string moreJson.baz.qux; // string moreJson.bool; // boolean console.log("moreJson is ComplexJson"); } else { console.log("moreJson is not a ComplexJson"); }

...then we will see the following output when we run the compiled code again:

# Run the compiled code $ node dist/index.js moreJson is not a ComplexJson

In this way, we can confirm that our narrowing function has worked and we can be sure that our moreJson object adheres to the ComplexJson type requirements.

Summary

Today's post was a review of the unknown type and to to see how we can use it to help us write safer code.

We looked at type narrowing, type predicates, and how we can use io-ts to help us decode our types and validate them.

I personally have misused unknown before by overriding the benefits with type casting, so it has been nice to review and write about the more common use cases for the unknown type. May you not make the same mistakes that I did when first encountering the type!

Resources and further reading

Photo credit: karsten116

Personal image

Dennis O'Keeffe

@dennisokeeffe92
  • Melbourne, Australia

Hi, I am a professional Software Engineer. Formerly of Culture Amp, UsabilityHub, Present Company and NightGuru.
I am currently working on Visibuild.

1,200+ PEOPLE ALREADY JOINED ❤️️

Get fresh posts + news direct to your inbox.

No spam. We only send you relevant content.

A Practical Look At The "Unknown" Type In Typescript

Introduction

Share this post