Comprehensive Guide to TypeScript Types

Comprehensive Guide to TypeScript Types

TypeScript is a strongly typed superset of JavaScript that adds static typing to the language. This ensures that type-related errors are caught during development, making your code more robust and easier to maintain. One of the most important features of TypeScript is its powerful type system, which helps developers write safer and more understandable code.

In this tutorial, we’ll explore TypeScript types in-depth, covering everything from basic types to advanced concepts. By the end, you’ll have a solid understanding of how to use TypeScript’s type system to write safer, cleaner, and more maintainable code.


1. Primitive Types

TypeScript provides a number of built-in types that are derived from JavaScript’s primitive types. These include:

  • number
  • string
  • boolean
  • null
  • undefined
  • symbol
  • bigint

Here’s an example of using these primitive types:

let isActive: boolean = true;
let userName: string = "John Doe";
let userAge: number = 30;
let userSymbol: symbol = Symbol("user");
let bigIntValue: bigint = 9007199254740991n;
let empty: null = null;
let notDefined: undefined = undefined;

2. Type Inference

TypeScript can often automatically infer the type of a variable, meaning you don’t always have to explicitly define it.

let name = "Alice";  // TypeScript infers 'name' to be a string
let age = 25;        // 'age' is inferred as a number

While you can rely on type inference, explicitly declaring types can make your code more readable and prevent unexpected bugs.

3. Arrays and Tuples

TypeScript allows you to specify types for arrays and tuples.

Arrays

You can define the type of elements inside an array like this:

let names: string[] = ["Alice", "Bob", "Charlie"];
let ages: number[] = [25, 30, 35];

This ensures that the array can only hold elements of a specific type.

Tuples

Tuples allow you to specify a fixed-length array with different types for each element:

let user: [string, number] = ["Alice", 25];

This means user is a tuple where the first element is a string and the second element is a number.

4. Enums

Enums are a way to define a set of named constants. They are useful when you have a group of related values.

enum Direction {
  Up,
  Down,
  Left,
  Right
}

let move: Direction = Direction.Up;

Enums can also have custom values:

enum StatusCode {
  Success = 200,
  NotFound = 404,
  ServerError = 500
}

let status: StatusCode = StatusCode.NotFound;

5. Union and Intersection Types

Union and intersection types allow you to combine multiple types into one.

Union Types

Union types allow a variable to be one of several types.

let id: string | number;
id = "123";  // valid
id = 123;    // valid

In this example, the id can be either a string or a number.

Intersection Types

Intersection types combine multiple types into one. All properties from the combined types must be present.

interface Person {
  name: string;
}

interface Employee {
  employeeId: number;
}

let employee: Person & Employee = {
  name: "Alice",
  employeeId: 123
};

In this case, employee must have both a name and an employeeId.

6. Type Aliases

Type aliases allow you to create a custom name for a type, which can be especially useful when dealing with complex types.

type ID = string | number;

let userId: ID = "123";  // valid
let anotherUserId: ID = 456;  // valid

Type aliases can also be used for object types:

type User = {
  name: string;
  age: number;
};

let user: User = {
  name: "Alice",
  age: 25
};

7. Literal Types

Literal types are types that represent specific values. They are particularly useful when combined with union types.

let status: "success" | "error" | "loading";
status = "success";  // valid
status = "loading";  // valid

This restricts status to only the specified literal values.

8. Type Assertions

Type assertions are a way to tell TypeScript to treat a value as a specific type, even if TypeScript cannot automatically infer it.

let someValue: any = "This is a string";
let strLength: number = (someValue as string).length;

Type assertions are helpful when dealing with dynamic content, such as API responses.

9. Functions

TypeScript allows you to specify the types of parameters and the return value of a function.

function add(a: number, b: number): number {
  return a + b;
}

let result = add(2, 3);  // result is a number

You can also define optional and default parameters:

function greet(name: string, greeting: string = "Hello"): string {
  return `${greeting}, ${name}!`;
}

greet("Alice");  // "Hello, Alice!"
greet("Bob", "Hi");  // "Hi, Bob!"

10. Generics

Generics allow you to write reusable code by creating functions and classes that can work with any type.

function identity<T>(arg: T): T {
  return arg;
}

let output = identity<string>("Hello");
let output2 = identity<number>(42);

Generics are a powerful feature that provides flexibility while maintaining type safety.

11. Interfaces

Interfaces define the structure of an object. They are similar to type aliases but provide more flexibility when working with object-oriented programming patterns.

interface User {
  name: string;
  age: number;
  isActive: boolean;
}

let user: User = {
  name: "Alice",
  age: 25,
  isActive: true
};

Interfaces can also extend other interfaces:

interface Employee extends User {
  employeeId: number;
}

let employee: Employee = {
  name: "Bob",
  age: 30,
  isActive: true,
  employeeId: 123
};

12. Classes and Interfaces

In TypeScript, you can implement interfaces in classes, ensuring that the class adheres to a certain structure.

interface User {
  name: string;
  age: number;
  greet(): string;
}

class Person implements User {
  constructor(public name: string, public age: number) {}

  greet() {
    return `Hello, my name is ${this.name}`;
  }
}

let person = new Person("Alice", 25);
console.log(person.greet());  // "Hello, my name is Alice"

This ensures that Person has the name, age, and greet properties defined in the User interface.

13. Type Guards

Type guards are used to narrow down the type of a variable within a conditional block.

function isString(value: any): value is string {
  return typeof value === "string";
}

function printLength(value: string | number): void {
  if (isString(value)) {
    console.log(value.length);
  } else {
    console.log(value.toString().length);
  }
}

Type guards make your code more type-safe by helping TypeScript understand what type a variable holds at runtime.

14. Mapped Types

Mapped types allow you to create new types by transforming properties of an existing type.

type User = {
  name: string;
  age: number;
};

type ReadOnlyUser = {
  readonly [K in keyof User]: User[K];
};

let user: ReadOnlyUser = {
  name: "Alice",
  age: 25
};

// user.name = "Bob";  // Error: Cannot assign to 'name' because it is a read-only property.

15. Utility Types

TypeScript provides several utility types to help with common transformations of types.

  • Partial<T>: Makes all properties optional.
  • Required<T>: Makes all properties required.
  • Readonly<T>: Makes all properties read-only.
  • Pick<T, K>: Picks a set of properties from T.
  • Omit<T, K>: Omits a set of properties from T.

Example of Partial<T>:

interface User {
  name: string;
  age: number;
}

let user: Partial<User> = {
  name: "Alice"
};

Conclusion

TypeScript’s type system is incredibly powerful and can significantly improve your development experience by catching errors at compile time, improving code readability, and making your codebase easier to maintain. This tutorial has covered the fundamentals of TypeScript types, from basic types to advanced concepts like generics, utility types, and mapped types. By mastering these features, you’ll be able to write more robust, maintainable, and scalable code in TypeScript.

Keep exploring and experimenting with TypeScript to discover even more ways to leverage its type system for your projects!

Featured photo by Timothy Cuenat on Unsplash