Typescript - Any vs Unknown

2021-07-215 min

Today's article is shortly about the differences between Typescript any and unknown types. Besides that, we will figure out which type should we use for which cases. To do this we need to start with a bit of knowledge about what is 'any' and 'unknown'.

Unknown

While working on applications there are cases where we are not sure about the data type. It might be when we fetch the data from an API, or we work with not typed 3rd party library or in other different cases.

The point is that we do not know the data shape and this is the situation when we can use the 'unknown' type. If we type the data as an 'unknown', then Typescript treats this data with a certain degree of distrust.

So what does it mean? Anything is assignable to 'unknown', but 'unknown' isn't assignable to anything but itself

It means that we can still assign other type values to the variable

let someValue: unknown;
someValue = 12345;
someValue = 'abv';
someValue = true;
etc.

but we cannot use it easily. So if we try to use some property on this variable we will receive an error.

const someValue: unknown;
someValue.foo // Object is of type 'unknown'.ts(2571)

Let's see another example

let someValue: unknown;
someValue = 'abv';

someValue.length // Object is of type 'unknown'.(2571)

We still cannot use methods/properties available on the string even if we assign a string value to the variable which was initially typed as 'unknown'.

We can't assign an 'unknown' type variable value to another variable with a defined type

let someValue: unknown;
someValue = 'example text'
const text: string = someValue; // Type 'unknown' is not assignable to type 'string'.ts(2322)

We also cannot pass the 'unknown' variable to the function which expects another type of variable

const maybeNumber: unknown;
const multiplyBy10 = (num: number) => num * 10;

multiplyBy10(maybeNumber); // Argument of type 'unknown' is not assignable to parameter of type 'number'.ts(2345)

So how can we play with unknown? To be safe here and be able to do anything with 'unknown' type we can introduce typeof checks, comparison checks, or more advanced type guards.

Typeof checks

declare const unknownVariable: unknown;

if (typeof unknownVariable === 'string') {
  // Now TypeScript knows that unknownVariable is a string
  const stringValue: string = unknownVariable; // OK
  const booleanValue: boolean = unknownVariable; // Type 'string' is not assignable to type 'boolean'.ts(2322)
}

Comparison checks

declare const unknownVariable: unknown;

if (unknownVariable === 'Expected string') {
  // Now TypeScript knows that unknownVariable is a string
  const stringValue: string = unknownVariable; // OK
  const booleanValue: boolean = unknownVariable; // Type 'string' is not assignable to type 'boolean'.ts(2322)
}

Type guards

Another way to check what kind of variable do we have is using ** type guard.** First, let's take a look at an example without a type guard

let a: unknown

const multiplyBy10 = (num: number) => num * 10
multiplyBy10(a) // Argument of type 'unknown' is not assignable to parameter of type 'number'.ts(2345)

To be able to use multiplyBy10 with an unknown type variable, we need to ensure that we actually can do it. So here we will introduce a function that checks a type for us

const isNumber = (x: unknown): x is number => typeof x === "number";

and a whole code example here:

let a: unknown

const multiplyBy10 = (num: number) => num * 10;
const isNumber = (x: unknown): x is number => typeof x === 'number';

multiplyBy10(a); // Argument of type 'unknown' is not assignable to parameter of type 'number'.ts(2345)
isNumber(a) && multiplyBy10(a); // Ok

As you can see above if we use our type guard first, TS ensures that a function argument is a type number and we can use it with multiplyBy10 function.

Type guards can be more advanced and personalized like below:

// Declare interface
interface Developer {
    name: string
    language: 'JavaScript' | 'Python' | 'C#'
}

// Declare 'unknown' variable
declare const unknownVariable: unknown;

// Declare a type guard to check if passed argument is type Developer
const isDeveloper = (human: any): human is Developer => {
    return (human as Developer).language !== undefined
}

// Use the type guard
if (isDeveloper(unknownVariable)) {
    // Properties defined on Developer interface are now available for unknownVariable
    unknownVariable.language
}

After a check with 'isDeveloper' type guard function - all properties defined on 'Developer' interface are now available for unknownVariable

Zrzut ekranu 2021-07-21 o 16.05.18.png

Any

Considering all I wrote about the unknown the difference is that any can be assigned to everything and everything can be assigned to any.

The main issue is that 'any' is totally unsafe. You can get a totally different shape of the data than you expect but 'any' let you do anything with it. You take responsibility.

Let's see an example

declare const someVal: any

someVal.doSomething().shouldWorkIGuess().length

So as you can see 'any' type allows you to do anything and gives you access to everything even if it does not exist on the variable or does not make sense at all.

When we should use any?

I know how daily programming looks like... Sometimes we struggle with 3rd party lib and its types or even worse - no types, or we are migrating from JS to TS. This is the place where sometimes 'any' is helpful.

Typescript docs about any

The 'any' type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type checking during compilation.

After all, remember that all the convenience of 'any' comes at the cost of losing type safety. Type safety is one of the main motivations for using TypeScript and you should try to avoid using any when not necessary.

Conclusion

I suggest you avoid 'any' type if it's possible.

The main difference between 'unknown' and 'any' is type safety. Be honest - type safety is probably the main reason to use Typescript so keep that in mind.