- Published on
Understanding TypeScript's Type Hierarchy From Any to Never
- Authors
- Name
- Yinhuan Yuan
Introduction
TypeScript, a statically typed superset of JavaScript, introduces a rich type system that helps developers write more robust and maintainable code. At the heart of this system lies a fascinating type hierarchy. In this post, we'll explore this hierarchy from top to bottom, understanding how different types relate to each other and how you can leverage this knowledge in your TypeScript projects.
- The Top: any and unknown
- The Middle: Object Types and Primitives
- The Bottom: never
- Union and Intersection Types
- Arrays, Tuples, and Readonly Types
- Enums
- Conclusion
any
and unknown
The Top: At the very top of TypeScript's type hierarchy, we find two special types: any
and unknown
.
any
The any
type is the most flexible in TypeScript. It essentially opts out of type checking, allowing you to do anything you want with variables of this type. While useful in certain scenarios (like working with dynamic content or migrating from JavaScript), overuse of any
can negate the benefits of using TypeScript.
let anyVar: any = 4
anyVar.nonExistentMethod() // No error
unknown
Introduced in TypeScript 3.0, unknown
is a type-safe counterpart of any
. It's the type-safe top type, meaning you can assign anything to an unknown
variable, but you can't do anything with it without first asserting or narrowing its type.
let unknownVar: unknown = 4
// unknownVar.toFixed(); // Error: Object is of type 'unknown'
if (typeof unknownVar === 'number') {
unknownVar.toFixed() // OK
}
Null and Undefined
null
and undefined
are subtypes of all other types when the strictNullChecks
flag is not used. With strictNullChecks
(recommended), they're only assignable to themselves and any
(and undefined
is also assignable to void
).
let nullValue: null = null
let undefinedValue: undefined = undefined
Void
void
represents the return type of functions that don't return a value:
function logMessage(message: string): void {
console.log(message)
}
The Middle: Object Types and Primitives
In the middle of the hierarchy, we find various object types and primitives.
Object Types
These include interfaces, classes, and literal object types. They form the backbone of TypeScript's structural type system.
interface Person {
name: string
age: number
}
class Employee implements Person {
constructor(
public name: string,
public age: number
) {}
}
let person: Person = new Employee('Alice', 30)
Primitive Types
TypeScript includes several primitive types that correspond to the basic types in JavaScript:
number
string
boolean
symbol
bigint
let num: number = 42
let str: string = 'Hello, TypeScript!'
let bool: boolean = true
never
The Bottom: At the very bottom of the type hierarchy is the never
type. It represents the type of values that never occur. You might use never
when you're sure that something is impossible.
function throwError(message: string): never {
throw new Error(message)
}
function infiniteLoop(): never {
while (true) {}
}
Union and Intersection Types
TypeScript allows you to combine types using union (|
) and intersection (&
) operators.
type StringOrNumber = string | number
type NameAndAge = { name: string } & { age: number }
Arrays, Tuples, and Readonly Types
TypeScript provides robust support for array types, tuples, and readonly variations of these.
Arrays
In TypeScript, you can define an array type in two ways:
let numbers: number[] = [1, 2, 3]
let strings: Array<string> = ['a', 'b', 'c']
Tuples
Tuples are arrays with a fixed number of elements whose types are known:
let tuple: [string, number] = ['hello', 42]
Readonly Arrays and Tuples
TypeScript allows you to create readonly versions of arrays and tuples:
let readonlyArray: ReadonlyArray<number> = [1, 2, 3]
// Or using an alternative syntax:
let anotherReadonlyArray: readonly number[] = [4, 5, 6]
let readonlyTuple: readonly [string, number] = ['hello', 42]
Readonly arrays and tuples cannot be modified after creation, providing an extra layer of immutability to your code.
Enums
Enums allow us to define a set of named constants. TypeScript provides both numeric and string-based enums:
enum Direction {
Up,
Down,
Left,
Right,
}
enum Color {
Red = '#FF0000',
Green = '#00FF00',
Blue = '#0000FF',
}
Enums can be particularly useful for representing a fixed set of options or states in your application.
Basic Enum Syntax
Here's the basic syntax for creating an enum in TypeScript:
enum Direction {
North,
South,
East,
West,
}
By default, enums will initialize the first value to 0 and add 1 to each additional value:
console.log(Direction.North) // 0
console.log(Direction.South) // 1
console.log(Direction.East) // 2
console.log(Direction.West) // 3
Customizing Enum Values
You can set custom values for enum members:
enum StatusCodes {
OK = 200,
BadRequest = 400,
Unauthorized = 401,
PaymentRequired = 402,
Forbidden = 403,
NotFound = 404,
}
console.log(StatusCodes.OK) // 200
console.log(StatusCodes.NotFound) // 404
String Enums
TypeScript also supports string enums. These are useful when you want to give a more meaningful value to your enum members:
enum Direction {
North = 'NORTH',
South = 'SOUTH',
East = 'EAST',
West = 'WEST',
}
console.log(Direction.North) // "NORTH"
Heterogeneous Enums
While it's not common, TypeScript does allow mixing string and numeric members in the same enum:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = 'YES',
}
However, it's generally recommended to stick to either all numeric values or all string values for consistency.
Enum as Types
One of the powerful features of enums is that they can be used as types in TypeScript:
enum UserRole {
Admin,
Editor,
Viewer,
}
function checkAccess(user: { role: UserRole }) {
switch (user.role) {
case UserRole.Admin:
console.log('Welcome, Admin!')
break
case UserRole.Editor:
console.log('You can edit, but not delete.')
break
case UserRole.Viewer:
console.log('You can only view.')
break
}
}
checkAccess({ role: UserRole.Admin }) // "Welcome, Admin!"
Reverse Mappings
Numeric enums come with a handy feature called reverse mapping. This means you can access the enum member name by its value:
enum Direction {
North,
South,
East,
West,
}
console.log(Direction[2]) // "East"
Note that this doesn't work for string enums.
const Enums
TypeScript provides a special kind of enum called a const enum
. These are completely removed during compilation and inlined at use sites:
const enum Direction {
North,
South,
East,
West,
}
let dir = Direction.South
This compiles to:
let dir = 1 /* South */
const enums
can provide a performance benefit, but they can't be used in all scenarios, such as when you need to iterate over an enum.
Computed and Constant Members
Enum members can be constant or computed. Constant members are calculated at compile time, while computed members are calculated at runtime.
enum FileAccess {
// constant members
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = '123'.length,
}
Ambient Enums
When working with enums from external libraries, you might encounter ambient enums. These are used to describe the shape of already existing enum types.
declare enum Enum {
A = 1,
B,
C = 2,
}
Best Practices
- Use PascalCase for enum names and enum members.
- Consider using string enums for better readability in debugging and logging.
- Use
const enums
when you don't need to iterate over the enum and want better performance. - Avoid using heterogeneous enums unless you have a specific reason to do so.
- When possible, specify values for all enum members to avoid unintended consequences of auto-incrementing values.
Conclusion
Understanding TypeScript's type hierarchy is crucial for writing effective TypeScript code. From the all-permissive any
at the top, through the rich landscape of object and primitive types in the middle, down to the never
type at the bottom, this hierarchy provides the foundation for TypeScript's powerful type system.
By leveraging this hierarchy, you can write more precise, safer, and self-documenting code. Remember, the goal is to use the most specific type possible for your use case, avoiding any
when possible and using unknown
when you need top-type behavior.
Happy TypeScripting!