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.
Table of Contents
What is the Unknown Type?
Let’s first take a look at the documentation. According to TypeScript
TypeScript docs
unknown
is the type-safe counterpart ofany
. Anything is assignable tounknown
, butunknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing
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
- TypeScript | The Unknown Type Guide
- TypeScript | Organizing and Storing Types and Interfaces
- TypeScript | Double Question Marks (??) – What it Means
- TypeScript | Objects with Unknown Keys and Known Values
- TypeScript | Union Types – Defining Multiple Types
- TypeScript | Declare an Empty Object for a Typed Variable
- TypeScript | Union Types vs Enums
- TypeScript | Convert Enums to Arrays
Did you like this TypeScript tip?
Share your thoughts by replying on Twitter of Become A Better Programmer or to personal my Twitter account.