Two great things about TypeScript are its modern inferred type system and its ability to interact with JavaScript code. A strong type system helps build robust applications that can easily be refactored and maintained, while JavaScript interop gives developers access to the enormous JavaScript ecosystem including all the modules available on NPM.
However, these two features are often in conflict with each other. JavaScript is a dynamic language without static types, so how can a statically typed language like TypeScript interact with non-typed JavaScript code?
One solution is creating declaration files to annotate JavaScript code with TypeScript types. Tools like DefinitelyTyped retrofit types onto many NPM modules, however there will always be times when TypeScript doesn’t know the types for JavaScript code. The language caters for this scenario with two builtin types: any
and unknown
. They are similar but have subtle differences that can have a big impact on the reliability of your TypeScript code. The TLDR is you should always use the unknown
type over the any
type. Here’s why:
any
typeLike the name suggests, The any
type represents values that can be of any type. TypeScript will treat the value like it’s any conceivable type possible. That includes primitive types like string
and number
as well as complex types like Date
and objects. any
effectively turns off all type checking. Anything goes, and that's the problem with the any
type. As the value can be absolutely anything and the compiler cannot check for errors, runtime errors will be much more likely. If the any
type was personified, it would look something like this:
any
effectively turns off the the benefits of TypeScript’s type system and you may as well be writing pain JavaScript with no type safety.
So why have the any
type in TypeScript at all? One major reason is that until unknown
(see below), any
was the only way to allow TypeScript to interact with JavaScript code that didn’t include type annotations via a declaration file. If you import non-typed JavaScript code into a TypeScript project, the type system has to assign the functions and values a type. But what type to use as the values could be anything? Hence the any
type was created.
any
begets any
any
also has the nasty habit of spreading throughout a codebase unless it’s used with strict discipline. If any
is used as the return value of a function it's very easy for code that imports that function to also start using any
. Take this code snippet for example that leans on TypeScript’s inferred typing rather than explicit typing:
/* --- declarations.d.ts --- */
declare module "some-js-module";
/* --- main.ts --- */
// An NPM module without TypeScript types
import { getValue } from "some-js-module";
// No return type set, so TypeScript will infer the type from the code
function process(input: number) {
// `value` is `any`
const value = getValue(input);
// Returns an inferred type of any
return value.toUpperCase();
}
Because the process
function did not explicitly set a type, it’s return type is now any
because it returned the value of getValue
(i.e. any
). If another section of the project imported the process
function the same problem would happen.
One solution is to use a tool such as eslint to check that all functions explicitly set their return type:
function process(input: number): string {
// `any` can be cast to any type, in this case `string`
const value: string = getValue(input);
return value.toUpperCase();
}
The type signature of process
now returns a string
, however this is a lie! We don’t know for certain what type getValue
returns and now we have the problem that any code that calls process
cannot trust the return type it claims. The integrity of the type system starts to break down as you can no longer trust it.
What we need is a better way to interact with values of unknown types that doesn’t erode the benefits of TypesScript’s type system. That’s where the unknown
type comes in.
unknown
typeIn my experience, 99% of the time any
is really used to communicate that we don’t know what type the value is - it’s completely unknown to us. The unknown
type was introduced in TypeScript 3.0 just over 5 years after the language hit 1.0. As it’s relatively new it’s not as widely used as its any
cousin and that’s a shame because unknown
enforces stricter type checking than any
. It also solves the problem of any
s propagating throughout a codebase and diluting the type system. It’s time for another cheesy comic:
To put it another way, any
will pretend to be any type imaginable and effectively turn off type checking, whereas unknown
forces you to prove that a value is a certain type before TypeScript will treat it like that type. The way to turn an unknown
value into a specific type is done through Type Guards.
Type Guards are used to test if a value is a specific type and it’s how we can convert an unknown
value into something useable. This is a simple example of a type guard that checks if a value is a string:
function isString(value: any | unknown): value is string {
return typeof(value) === "string";
}
Type Guards are functions that return a boolean
result, however their return type uses the is
keyword and not a simple a boolean
. The isString
Type Guard we created takes a value (either any
or unknown
) and returns a boolean
. If the returned value is true
, TypeScript will treat references of value
as a string within the relative scope (such as within an if
statement). Let’s use our isString
Type Guard and unknown
s in the process
function:
/* --- declarations.d.ts --- */
declare module "some-js-module";
/* --- main.ts --- */
// An NPM module without TypeScript types
import { getValue } from "some-js-module";
function isString(value: any | unknown): value is string {
return typeof(value) === "string";
}
function process(input: number): string {
// We have cast `any` to `unknown`
const value: unknown = getValue(input);
if (isString(value)) {
// `value` is treated as a string
return value.toUpperCase();
}
throw new Error(
"Expected a string from getValue, " +
`got ${typeof(value)} instead`
);
}
The isString
Type Guard tells TypeScript that value
is in fact a string
and all the usual code completion and type checking will work as normal. To illustrate this point, TypeScript would throw a build error if we tried to use any string properties on value
before we proved it was a string:
function process(input: number) {
const value: unknown = getValue(input);
if (isString(value)) {
// OK
return value.toUpperCase();
} else {
// BUILD ERROR: 'Object is of type 'unknown'
return value.toLowerCase();
}
}
Type Guards when used along side unknown
provide much more reliable code with less runtime errors than when compared to using any
. However, it’s important to throughly test your Type Guard functions as the TypeScript type system completely trusts what type casting the functions perform.
Hopefully you now have a better understanding of any
, why it exists in TypeScript and how unknown
was added to the language to address any
’s shortcomings. Because unknown
helps to reduce runtime errors and maintain trust in the type system I high recommend using it over any
where possible. If you must use any
, casting it to unknown
is a good compromise.