In my years of writing TypeScript, I've encountered areas of the language where myself and others often get tripped up. One example are three special types:

In this post, I want to give a rundown of what these types mean and how to use them from a practical perspective.

any

any is the most straightfoward of the three special types. A value of type any can be, well, anything! You can think of variables with the type any as having type-checking disabled. We say that all types are assignable to any, and that any is assignable to any type.

// All of the following is legal, even if it's not valid/useful

// "foo" could be any value of any type. That's what we mean when we say all 
// types "are assignable" to `any`
let x: any = "foo";
let y: number = x;
x[1];
x.someProperty;
x.someMadeUpMethodName()

const square = (n: number) => n*n;
// mySquare will be NaN here, not good!
// `any` can be used in the place of any type. `square` expects a value of type
// `number` but we substitute in `any`. This is what we mean when we say `any`
// "is assignable to" any type
let mySquare = square(y);

Typically, any is used as an escape-hatch from the type system when it'd be too difficult or tedious to convince TypeScript that some code is correct.

unknown

unknown is the "type-safe" version of any. You can assign anything to a variable of type unknown, but basically no operations are permitted on an unknown value except for assignment to any and type-casting.

let x: unknown = "foo"

// ILLEGAL! 
let y: number = x;

// LEGAL! Anything is assignable to `any`.
let z: any = x; 
// LEGAL! Type-casting `unknown` is allowed (and generally necessary).
let w: string = x as string;

// Using `unknown` in any way is also illegal.
// The following 3 lines would cause a compiler error:
let result = x + x;
let propertyAccess = x.length;
let callingMethods = x.toString();
// and just about anything else you can think of...

The utility of unknown is that it lets us write code that accepts values of any type, but forces us to more-or-less safely assert the type of the value before the compiler will let us use it. TypeScript offers many different ways for asserting or otherwise specifying the type of a value, and the details are outside the scope of this post. But, you can read all about it in the TypeScript manual page on narrowing.

The characters so far:

never

You can think of never as the "opposite" of any and unknown. Whereas any and unknown are types that contain all possible values, never is a type that contains no values.

Unsurprisingly, this means that the only things you can assign to a variable of type never are more expressions with the type never. The use of the term "expression" is very purposeful, because there is no such thing as a value with type never, by definition.

Less intuitively, an expression of type never can be assigned to any variable of any type. For example:

function foo(): never {
  throw "err";
}

// This is legal!
const bar: number = foo();

This is a kind of vacuous truth. It's true, in an unsatisfying way, that bar is being assigned a number because it's not being assigned any value at all.

If it's not obvious to you why never is useful or even necessary, you are not alone. Unless you're writing advanced conditional types (a topic for another day) you are pretty unlikely to need never. Though, you may encounter it from time to time if you're using a library with complex generic types and you don't specify the type arguments correctly.

never serves some important theoretical purposes (another topic for another day), but the most practical usecase I can think of for most people is exhaustiveness checking. Here's an example:

interface ComputerScienceStudent {
  major: 'cs';
  favoriteLanguage: string;
}

interface ArchitectureStudent {
  major: 'arch';
  favoriteBuilding: string;
}

interface MathStudent {
  major: 'math'
  favoriteEquation: string;
}

type Student =
 | ComputerScienceStudent
 | ArchitectureStudent
 | MathStudent;

function greetStudent(student: Student) {
  switch (student.major) {
    case 'cs':
      console.log(`Student's favorite language is ${student.favoriteLanguage}`);
      return;
    case 'arch':
      console.log(`Student's favorite building is: ${student.favoriteBuilding}`);
      return;
    case 'math':
      console.log(`Students favorite equation is: ${student.favoriteEquation}`);
      return;
    default:
      // THIS IS THE IMPORTANT BIT
      // This assignment will produce a compiler error if you are not
      // handling every possible variant of `Student`.
      const _check: never = student;
      return _check
  }
}

What we're doing is producing a statement that is only valid if it's impossible for the default case of the switch statement to actually run. The default case will never run if all the variants of Student are processed by a preceding branch of the switch. So, if we were to add a variant to the Student type later and fail to update greetStudent, we'd get a compile error because it would be possible to assign a real value to the _check variable.

Summary