TypeScript | The Unknown Type Guide

TypeScript provides a variety of type definitions. Some of them are common such as strings, numbers, arrays, booleans to more custom interfaces and classes. New types came with the release of TypeScript 3.0. One of them is the unknown type which we will cover in this article.

What is the Unknown Type?

Let’s first take a look at the documentation. According to TypeScript

unknown is the type-safe counterpart of any. Anything is assignable to unknown, but unknown isn’t assignable to anything but itself and any without a type assertion or a control flow based narrowing

TypeScript docs

In other words, the unknown type is a restrictive version of the type any. To make things simple, let’s break the definition down in pieces and illustrate it with some code.

Anything is assignable to unknown.

As it says, absolutely anything can be assigned to an unknonwn variable type. This means everything will work at the moment of compiling the following code.

let random:unknown;

random = 'Hello World!';
random = {};
random = 7;
random = null;
random = Math.random();
random = ['USA', 'Colombia', 'India', 'Canada'];
random = new Country();
random = undefined;

Unknown isn’t Assignable to Anything

We cannot assign the value of an unknown variable type to another typed variable different from unknown and any. We are going to use the random variable from the previous example.

let random: unknown; 
let foo: unknown;
let bar: any;

foo = random;  // Correct
bar = random;  // Correct

Attempting to assign random to another typed variable will fail.

let stringValue: string;
let numberValue: number;
let arrayValue: [];
let countryValue: Country;

stringValue = random;  // fail
numberValue = random;  // fail
arrayValue = random;   // fail
countryValue = random; // fail

Also, you will see the following error messages:

Type 'unknown' is not assignable to type 'string'.ts(2322)

Type 'unknown' is not assignable to type 'number'.ts(2322)

Type 'unknown' is not assignable to type '[]'.ts(2322)

Type '{}' is missing the following properties from type 'Country': name, population, continent ts(2739)

Unknown is Assignable to Object

If you are curious and start testing whether or not you cannot assign unknown to any other type, you probably notice the previous example didn’t include the scenario of assigning random to an object typed variable.

let objectValue: {};  // this is the same as "let objectValue: Object;"
objectValue = random; // Won't fail

Attempting to compile the previous code snippet will work and won’t throw any errors.

How is this possible?

In JavaScript, just about anything is an object with the exception of the following primitive types: string, number, bigint, boolean, undefined, symbol, and null. Therefore, when defining a variable the Object type, we expose the functions and properties defined by the Object type. However, since there is not a specific interface or set of properties for the Object type, we could assign just about anything to the object, including an unknown typed variable.

let objectValue: Object;
objectValue = random;
objectValue = 'asf';
objectValue = null;
objectValue = undefined;
objectValue = 14;
objectValue = new Country();

Using the unknown Type in Intersections Types

It is time to look at other behaviors the unknown type has that you might not be aware of. We are going to start with using the unknown type in intersection types.

If using the unknown type with intersection types, you will see everything “absorbs” or takes precedence over unknown.

type Type1 = unknown & string; // string
type Type2 = unknown & string[]; // string[]
type Type3 = unknown & unknown; // unknown
type Type4 = unknown & any; // any
type Type5 = unknown & null; // null
type Type6 = unknown & undefined; // undefined

That means, no matter the position where you use the unknown type, whether it is at the beginning or at the end in an intersection, the type will never be unknown unless the only types used in the intersection is unknown, making the usage of intersections unnecessary.

type Type7 = unknown & unknown;

Using the unknown Type in Unions Types

The behavior of the unknown type in union types is the opposite as if it were used in intersection types.

When using union types, the unknown type absorbs all the other types.

type Type1 = unknown | string; // unknown
type Type2 = unknown | string[]; // unknown
type Type3 = unknown | unknown; // unknown
type Type4 = unknown | any; // unknown
type Type5 = unknown | null; // unknown
type Type6 = unknown | undefined; // unknown

That means, no matter the position where you use the unknown type, whether it is at the beginning or at the end in a union, the type will always be unknown. Therefore, it becomes pointless to define a union type containing the unknown type as it will default to unknown.

Difference Between unknown Type and any Type

As previously mentioned, the unknown type is a more strict implementation of the type any. Although they seem to be similar at first, there are a few differences to take into account:

The Type any is Assignable to Anything

The type any provides more flexibility at the moment of assigning a value to other types of variables. It is possible to assign the value stored in an any typed variable to a string, boolean, number, interface, class, etc.

// examples of assigning values from an "any" typed variable to other 
// variables with different types
let anyRandom: any;

let textAny: string = anyRandom;                // no errors
let numberAny: number = anyRandom;              // no errors
let booleanAny: boolean = anyRandom;            // no errors

interface MyInterface {
  id: string;
}
let interfaceAny: MyInterface;
interfaceAny = anyRandom;                       // no errors

class Entity { 
  id: string;
}
let entityAny: Entity;
entityAny = anyRandom;                          // no errors

On the other hand, the unknown typed variable can only assign its value to another unknown or any type.

// None of the following examples will work
let unknownRandom: unknown;


let textUnknown: string = unknownRandom;         // fail
let numberUnknown: number = unknownRandom;       // fail
let booleanUnknown: boolean = unknownRandom;     // fail

interface MyInterface {
  id: string;
}
let interfaceUnknown: MyInterface;
interfaceUnknown = unknownRandom;                // fail

class MyEntity { 
  id: string;
}
let entityUnknown: MyEntity;
entityUnknown = unknownRandom;                   // fail

The Type any Can Call Methods or Constructors

When you use an object type such as a String, you can create a new instance of a String as well as calling String internal methods such as trim(), replace(), split(), or the most common toLowerCase().

When using any and unknown types, the type can be anything. However, any allows you to call itself, call internal methods or generate new instances of itself without getting any compilation errors:

let anyRandom: any;

// None of the following examples will not error during compilation even though
// we new the anyRandom variable is not a function or a custom object 
anyRandom.myMethod();   // no compilation errors
anyRandom();            // no compilation errors
unknownRandom.length;   // no compilation errors

Not getting compilation errors doesn’t mean the logic in the example above won’t fail during execution. To prevent unexpected errors during runtime, use the unknown type as it will immediately fail during compilation.

let unknownRandom: unknown;

// None of the following examples will error during compilation 
unknownRandom.myMethod();   // will have compilation errors
unknownRandom();            // will have compilation errors
unknownRandom.length;       // will have compilation errors

When to use unknown and any

In short, TypeScript is JavaScript with types. Simple and powerful concept. JavaScript provides a lot of flexibility, and that much flexibility comes with problems that could have been prevented during build time rather than during the execution of JavaScript code.

The purpose of TypeScript, besides providing types, is to be a guide to prevent unexpected behaviors that could have been prevented during development. This will feel more restrictive for many and will lose some of that flexibility. However, with TypeScript, you are still able to determine the level of flexibility you want to have during development.

The best way to think about using any is if you want to keep that flexibility. That flexibility is valuable for those looking to develop much quicker at the expense of running into errors that could have been prevented. Also, it is encouraged to use any when the developer has a good idea of what kind of values will be assigned to the any typed variable. However, this will make it complex for new developers in a project to determine what kind of values are used.

On the other hand, using unknown is a way to provide a level of flexibility, but making aware the developer that not everything can be attempted such as calling a toString() method even if the values inside the unknown allows it. This removes the number of unexpected errors that could happen when executing code.

In short, It is recommended to use unknown instead of any if you are looking to have a more predictable code.

Convert unknown Type to interface Type

The easiest way to tell TypeScript an unknown type is an interface is by using assertions.

interface Car {
  model: string;
  brand: string;
}

let random: unknown = { brand: 'Ford', model: 'Mustang' };
let car: Car = <Car>random;

However, this might not be the best way as it is possible the unknown object has properties that are not part of an interface. In case you are looking to check all properties of an unknown object are in an interface, it is recommended to generate a class off of the interface, generate a new instance of that class, extract the keys and do a check against each of the unknown‘s object properties. This should look something like the following example:

interface ICar {
  model: string;
  brand: string;
}

class Car implements ICar {
  model: string;
  brand: string;

  constructor(values?: Partial<Car>) {
    if (values) Object.assign(this, values);
  }

  static isCar(unknownObject: unknown): boolean {
    const carKeys = Object.keys(new Car({ model: 'test', brand: 'test' }));

    if (typeof unknownObject !== 'object') return false;

    for (const unknownKey in unknownObject) {
      const hasKey = carKeys.some((k) => k === unknownKey);

      if (!hasKey) return false;
    }

    return true;
  }
}

console.log(Car.isCar(<unknown>{ brand: 'Ford' })); // true
console.log(Car.isCar(<unknown>{ brand: 'Ford', model: 'Mustang' })); // true
console.log(Car.isCar(<unknown>{ brand: 'Ford', year: 2008 })); // false

Convert unknown Type to string

Depending on what you are looking to accomplish, there are a couple of alternatives.

Using String Assertions

Using assertions are the quickest way to tell TypeScript you know the unknown typed variable has a string value.

// Example to convert unknown type to string: using string assertion
let random: unknown = 'Hello World!';
let stringValue: string = random as string;

In theory, we are not converting anything as there is no reason to convert a value into a string value when it is already a string value.

Using the String Constructor

Another alternative is to use the String constructor. This will return a string primitive of any value provided in the constructor. Therefore, we could do the same with the an unknown typed variable.

// Example to convert unknown type to string: using String Constructor
let random: unknown = 'Hello World!';
let stringValue: string = String(random);

Caveat: Be careful when using String constructor, as anything will be converted into a string, whether it is a string, a boolean, a number, or even a function!

String(false); // it will be converted to 'false'
String(4); // it will be converted to '4'
String(function test() { }); // it will be converted to 'function test() {}'
String(undefined) // it will be converted to 'undefined'
String(null) // it will be converted to 'null'

Recommendation: Check the typeof the unknown typed variable

If your intention is to always assign the value of the unknown typed variable to a string variable, using the String constructor will be your best bet. However, it is recommended to check the type of the value of the unknown variable prior to using string assertions or String constructor to convert to a string. Therefore, it won’t be necessary to use those two techniques as we already checked the unknown type is a string.

let random: unknown = 'Hello World!';
let stringValue: string;

if (typeof random === 'string') {
  stringValue = random; // no need to use string assertions or String constructor
}

More TypeScript Tips!

There is a list of TypeScript tips you might be interested in checking out

Did you like this TypeScript tip?

Share your thoughts by replying on Twitter of Become A Better Programmer or to personal my Twitter account.