(You can also read the whole article on the TypeScript playground.)

Let’s start with a simple example.

let stringValue: string = "abcString";
let stringAsAny: any = "abcAny;"

stringValue.charAt(3); // will return "S"
stringAsAny.charAt(3); // will return "S", the same thing since stringAsAny is actually a string
stringValue.endsWith("g"); // will return true
stringAsAny.endsWith("g"); // will return true, the same thing since stringAsAny is actually a string

And we have the same thing for every methods or function that exists on or takes as string. Things change when we make a mistake.

let numberValue: number = 3;
numberValue.toExponential(2); // will return "3.00e+0" because toExponential gives the exponential notation of the number, obviously this makes sense only for number
stringValue.toExponential(2); // so it makes sense that this is a compiler error since toExponential is NOT a string method
stringAsAny.toExponential(2); // but this is not a compiler error, because any means "I know what I'm doing" (aka JS mode) to the compiler, it will still fail at runtime though

Sometimes we use any without noticing (linters are useful for this).

let a; // a is implicitly any
a = 3; // but TypeScript is smart so from now on a will be typed as number
a.toExponential(2); // no error, as intended
a.charAt(2); // typescript detects the error

a = "hello"; // 
a.toExponential(2); // typescript detects the error
a.charAt(2); // no error

In this cas it would have been better to declare a as number | string, like this:

let aa: number | string = 3; // TypeScript is too smart for us here and it breaks the example I wanted to make. We declare aa as `number | string` but it detects that we affect it a number so it is a number.
aa.toExponential(2); // that's why this works
aa.charAt(2); // but this doesn't
aa.toString(); // and this would work in any case since toString exists both on number AND string

Return a disjunctive type

So, let’s redo out example but with a function:

function numberOrString(): string | number {
  if(Math.random() > 0.5) {
    return "a";
  }
  else {
    return 3;
  }
}

You can try to remove the return type string | number and you’ll notice that TypeScript inferred the returned type to "a" | 3 which is true but too specific for our example so I put the return type myself and since string | number is compatible with "a" | 3 it works. Putting just “number” as the return type would have been a compiler error when we try to return "a".

Anyway, we now have a function that returns sometimes a number and sometimes a string, and we have no way to know in advance which, it is perfect for our example!

This example is a little contrived, but you can imagine that you get records from an API and the API sometimes return a number and sometimes a string.

let aaa = numberOrString(); // type of aaa is "number | string", good!
aaa.toExponential(2); // compiler error
aaa.charAt(2); // compiler error
aaa.toString(); // works

It’s a little weird that both toExponential and charAt gives a compiler error but since we don’t know if it’s a number or a string we can’t actually call any of those methods until we are sure of the type. Calling toString is fine since it exists on both type anyway.

if(typeof(aaa) == "string") {
  aaa.charAt(2); // in this branch typescript knows that aaa is a string because of the test
  aaa.toExponential(2); // and it also knows that it's NOT a number so this is a error
}
else {
  aaa.toExponential(2); // in this branch TypeScript knows that aaa is NOT a string, which means that it's a number since there is only 2 possibilities
}

If there were 3 possible return types:

function numberOrStringOrBoolean(): string | number | boolean {
  const rand = Math.random();
  if(rand < 0.3) {
    return "a";
  }
  else if(rand < 0.6) {
    return true;
  }
  else {
    return 3;
  }
}

let aaaa = numberOrStringOrBoolean();
if(typeof(aaaa) == "string") {
  aaaa.charAt(2); // in this branch typescript knows that aaaa is a string because of the test
  aaaa.toExponential(2); // and it also knows that it's NOT a number so this is a error
}
else {
  // and here aaaa is either a boolean OR a number
  let a = aaaa; // check the type of a by hovering over it
  if(typeof(aaaa) == "boolean") {
    // the compiler knows that aaaa is a boolean here
    aaaa;
  }
  else {
    aaaa;
  }
}

// obviously in a real example we would have just done
if(typeof(aaaa) == "string") {
  aaaa.charAt(2); // in this branch typescript knows that aaaa is a string because of the test
  aaaa.toExponential(2); // and it also knows that it's NOT a number so this is a error
}
else if (typeof(aaaa) == "boolean") {
  // the compiler knows that aaaa is a boolean here
  aaaa;
}
else {
  // the compiler knows that aaaa is a number here
  aaaa;
}

We showed that it’s better to use disjunctive types (“disjunction” means “or” and is represented in TypeScript by the character “|” like in number | string | boolean which means “number or string or boolean”)

We got a little off track here, we wanted to talk about the any type so let’s go back to it.

Using a Library

Most of the times when I am forced to use any it’s because I use a library that uses it. Usually the library could have avoided using it by make better typings but… we are stuck with it, let’s see how to get out of this problem to avoid the stringAsAny.toExponential(2) fiasco.

Let’s say we have a lib called SomeLib with a method getSomething which return an any

function getSomethingOld(): any {
  return numberOrStringOrBoolean(); // the library authors could have put "number | string | boolean" as the return type here, but it could also have been much more compilcated, it's probably much more complicated and that is why they used any
}

const SomeLib = {
  getSomething: getSomethingOld
}

let somethingNew = SomeLib.getSomething(); // no surprise here somethingNew is of type any

The problem with any values is that TypeScript is VERY permissive with them which means that you can do.

let somethingBorrowed: number = SomeLib.getSomething(); // no error here. any means "I know what I am doing" so TypeScript suppose that you know what you are doing. TypeScript is wrong here, you made a mistake but you told it you knew what you were doing, what's it supposed to do?

// The mistake could be more subtle
function addThree(n: number) {
  return n + 3;
}

addThree(SomeLib.getSomething()); // will return "a3" ("a" + 3), or 4 (true + 3, true is 1 in sums, don't get me started...), or 6 (3 + 3), which is probably not what we want, we would have liked if typescript said something like "This function expects a number here but you gave any which may or may not be a number", but that would contradict the "I know what I am doing" mantra so it justs stay silent

// to be safe you would have to do
if(typeof(somethingNew) == "number") {
  addThree(somethingNew); // in this branch somethingNew is of type number so it will always do what's reasonnable (adding number between them and NOT adding number and a string or a number and a boolean)
}

But TypeScript won’t say anything if you forget to check the type because any means “I know what I am doing”, which we sometime are… but not always

Lucky for use our savior is here and it is unknown!

function getSomethingBlue(): unknown {
  return numberOrStringOrBoolean();
}

// Let's redo our lib
const SomeBetterLib = {
  getSomething: getSomethingBlue
}

let somethingNew2 = SomeBetterLib.getSomething(); // somethingNew is of type unknown, but what does it mean?
let somethingBorrowed2: number = SomeBetterLib.getSomething(); // it means you can't do that, compiler error!
addThree(SomeBetterLib.getSomething()); // and that you can't do that either, compiler error again!

Actually you can’t do anything with an unknown! That’s the beauty of it. It seems kind of useless right? You’re right. Until you use the “as” operator.

let somethingBorrowed3: number = SomeBetterLib.getSomething() as number; // no compiler error but is it really what we want? We know that getSomething could also return a boolean or a string...
addThree(SomeBetterLib.getSomething() as number); // no error here, but we could be adding boolean and number so shouldn't there be an error?

Well, “as” is basically an other way to say “I know what I’m doing”, actually it’s a way to say “I mostly know what I’m doing” (note the “mostly”) because there is some checks done, we’ll get to those later.

The good thing with unknown, and why it’s better than any, is that we can’t forget about it, we have to explicitly “cast” (not really a cast, it’s a “type assertion”, we’ll see about that later) it.

To avoid using “as” everytime I use SomeBetterLib.getSomething() I can either wrap it into a function of my own or redo the types of the library (which is basically similar to wrapping it since I just use the types of the wrapping function at the declaration site).

function getSomethingWrapped() {
  return SomeBetterLib.getSomething() as number | boolean | string; // to deduce this type I had to look at the code or test it multiple times
}

Now I can use getSomethingWrapped without using any type assertions (“cast”).

So, to sum up: never use any. Use unkown instead.

Even the standard library makes this mistakes with JSON.parse()

let j = JSON.parse("{}"); // j is any here which means I can do
j.lol();
j.hello(); // without error, here the example si simple enough and we know it will fail at runtime but if JSON.parse() returned unknown we could not do this.

function JsonParseWrapped(jsonString: string) {
  return JSON.parse(jsonString) as unknown;
}
let jj = JsonParseWrapped("{}"); // j is any here which means I can do
jj.lol(); // error
jj.hello(); // error
(jj as number).toExponential(2); // works for the compiler but will obviously fail at runtime

The TypesSript “as” operator

Now let’s talk about “as”. You probably know that TypeScript is compiled (or transpiled) to JavaScript. You probably know that most of what makes TypeScript is stripped away during this transpilation step which means that, at runtime, TypeScript DOES NOT exists. The “as” operator doesn’t exists in JavaScript, it’s only a TypeScript notion. You can verify it easily.

(jj as number).toExponential(2); // TypeScript code
jj.toExponential(2); // transpiled JavaScript code of the previous line, "as" is nowhere to be seen
// That's why I said that "as" is like saying to the TypeScript compiler "I mostly know what I'm doing", why mostly? There is some checks done.

type X = { x: string; }
type Y = { y: number; }
let x: X = { x: "a" };
let y = x as Y; // we have an error here "Conversion of type 'X' to type 'Y' may be a mistake because neither type sufficiently overlaps [...]", basically TypeScript it telling that X and Y have nothing in common so it may be a mistake
let yy = x as unknown as Y; // but this does not give a compile error, it will still give a runtime error when you try to access yy.a.charAt but at least you have to really want it!

type Z = { x: string; z: boolean; }
let z = x as Z; // but here it's ok because X and Z have some fields in common, it will still fail at runtime though

But again, this is “as” only exists at compile time which means that if you make a wrong type assertion it will fail at runtime

Checking that you can safely convert a type into another is a complicated problem in the scope of data validation or input validation which I haven’t explored enough to talk about. ```