blog post

The Complete Guide to TypeScript Types

This blog post makes a deep dive into TypeScript's type system, exploring basic types, advanced types, and some special cases, with various examples to help you master TypeScript's type system.

Primitive types

TypeScript has 3 primitive types: number, boolean and string, same as in JavaScript. According to official TypeScript docs: we should always use lowercase names for primitive types, as Number, Boolean and String are special built-in types.

  • number - represents integer and float numbers under a single type, like: 5 or 5.2
  • boolean - represents a true of false value
  • string - represents text values, like "Some text here"

Here is how to define variables of primitive types:

typescript
const total: number = 100; const isActive: boolean = true; const userName: string = "Anton";

Alternatively we can define variables like this, where types will be inferred automatically:

typescript
const total = 100; const isActive = true; const userName = "Anton";

Enums

Enums in TypeScript provide a way to group a set of named constants together under a single type. Enums make the code easier to read and maintain for variables that have a defined set of values, instead of using magic numbers or strings. TypeScript supports both numeric and string enums.

By default, enums in TypeScript are numeric. Numeric enums map names to numbers, starting from zero and incrementing by one for each member. Although you can manually set the value of its members if needed.

typescript
enum Status { New, // 0 InProgress, // 1 Completed // 2 } const taskStatus: Status = Status.InProgress; console.log(taskStatus); // Outputs: 1

String enums, on the other hand, map names to strings. Unlike numeric enums, string enums must be manually initialized with a string value.

typescript
enum Status { New = "NEW", InProgress = "IN_PROGRESS", Completed = "COMPLETED" } const taskStatus: Status = Status.InProgress; console.log(taskStatus); // Outputs: IN_PROGRESS

Object Types

Object types in TypeScript groups a set of properties under a single type.

Object types can be defined either using type alias:

typescript
type User = { id: number; name: string; }; const user: User = { id: 1, name: "Anton" };

Or interface keyword:

typescript
interface Person { name: string; age: number; }; const person: Person = { name: "Anton", age: 30 };

Interfaces are extendable and can inherit from other interfaces. You can extend them by declaring the same interface multiple times, or by extending them using the extends keyword. Classes can implement interfaces.

The type alias in TypeScript can be used to define a type for a variety of structures, not just objects. This includes primitives, unions, intersections, tuples, and more. While type provides more flexibility in defining complex types, you cannot reopen a type to extend or add new properties.

Whether to use type or interface - it all depends on a specific needs of a project or a personal or team preference.

Functions

TypeScript enhances functions by allowing developers to specify types for parameters and return values. This ensures that functions are called with the correct types of arguments and that their return types are consistent with expectations.

typescript
function sayHello(name: string): string { return `Hello, ${name}!`; } const message: string = sayHello("Anton");

Functions can accept arguments and return a value using any types.

Arrays

Arrays in TypeScript allow to store multiple values of the same type in a list.

TypeScript allows array creation for primitive and object types:

typescript
const numbers: number[] = [1, 2, 3, 4, 5]; const names: string[] = ["Anton", "Jack", "Nick"]; type User = { id: number; name: string; }; const users: User[] = [ { id: 1, name: "Anton" }, { id: 2, name: "Jack" }, ];

Also you can use a generic array type Array<elementType> for creating arrays:

typescript
const scores: Array<number> = [1, 2, 3, 4, 5]; const cities: Array<string> = ["London", "New York", "Kyiv"];

Type Assertions

Type assertions in TypeScript allow you to tell the compiler that you know better the type of an object or variable than TypeScript can infer. Essentially, a type assertion is like a type cast in other languages, but it performs no special checking or restructuring of data. It has no runtime impact and is used purely by the TypeScript compiler for type checking.

You can use as keyword to perform type assertion:

typescript
const value: any = "some text here"; const length: number = (value as string).length;

Alternatively you can use <type> syntax:

typescript
const value: any = "some text here"; const length: number = (<string>value).length;

Important tip: you can't use <type> syntax in *.tsx files in React.

Because type assertions have no impact and type checking during runtime: there won't be an exception or null if the type assertion is wrong.

Union Types

Union type in TypeScript is a type that is formed from two or more other types. A variable of union type can have one of the types from the union.

Here's a simple example where a variable can hold either a number or a string:

typescript
let id: number | string; id = 100; // This code is correct id = "200"; // This code is correct // TypeScript Error: Type 'boolean' is not assignable to type 'string | number'. id = false;

Unions are particularly useful in function parameters, allowing functions to accept arguments of different types:

typescript
function printNumber(value: number | string) { console.log(`Number: ${value}`); } printNumber(100); // Works with a number printNumber("200"); // Also works with a string

When using unions, TypeScript will only allow to access properties or methods that are common to all types in the union. To use type-specific properties or methods, you need to use type guards:

typescript
function getLength(obj: string | string[]) { if (typeof obj === "string") { return obj.length; // Return length of a string } return obj.length; // Return legnth of string array }

Unions are also useful when combining string literals. A literal in TypeScript is a special type for constant string, for example:

typescript
let text: "TypeScript" = "TypeScript"; text = "TypeScript"; // This code compiles // TypeScript Error: Type '"JavaScript"' is not assignable to type '"TypeScript"'. text = "JavaScript";

By combining literals into unions you can express that a variable can only accept a certain set of values:

typescript
function print(value: string, align: "left" | "right" | "center") { // ... } print("TypeScript types", "left"); // TypeScript Error: "align" parameter should be one of: "left" | "right" | "center" print("TypeScript types", "midde");

Intersection Types

Intersection types in TypeScript allow you to combine multiple types into one. An intersection type can be created by combining multiple interface types using & operator:

typescript
interface Person { name: string; } interface ContactDetails { email: string; phone: string; } type Customer = Person & ContactDetails; const customer: Customer = { name: "Anton", email: "[email protected]", phone: "1234567890" };

In this example a Customer type is an intersection type that combines all properties from Person and ContactDetails interfaces.

Intersections are not limited just to interfaces, they can be used with other types as well, including primitives, unions, and other intersections.

Tuple Types

Tuple types in TypeScript is an array with a fixed number of elements and types at specific positions. Tuples are particularly useful when you need to return multiple values from a function or when you want to represent a data structure with a fixed number of elements of specific types.

typescript
const person: [string, number] = ["Anton", 30];

This tuple represents a person's information: name and age. The first element must be a string, and the second must be a number.

Tuples are particularly useful when you need to return multiple values from a function:

typescript
function getPersonDetails(personId: number): [number, string, string] { // Simulate returning a value from an API: return [personId, "Jack Sparrow", "Captain"]; } const [name, id, position] = getPersonDetails(1); console.log(name); // Jack Sparrow console.log(id); // 123 console.log(position); // Captain

Special Types: any, unknown

The any keyword in TypeScript tells the compiler to trust you about the type of a variable, and it won't try to check its type during compilation. A variable of type any is like a JavaScript variable, freely mutable and usable in any context.

Here is an example of using a variable of type any:

typescript
let data: any = "Hello, world!"; data = 42; // This code compiles without errors. // Runtime error! Numbers don't have a toLowerCase method. console.log(data.toLowerCase());

While there are certain scenarios, you should avoid any at all costs, use it only as the last available option.

A slightly better option is to use unknown type, which is a bit more type-safer. While it represents any value (just like any), TypeScript will ensure that you check the type of the variable before using it:

typescript
let data: unknown = "Hello, world!"; data = 42; // This code compiles without errors. // This won't compile: // console.log(data.toLowerCase()); if (typeof data === "string") { console.log(data.toLowerCase()); // Now it compiles! }

If you want to learn more about any and unknown I have a dedicates blog posts about them:

Special Types: never, null, undefined

The never type represents values that never occur. It's a type that is used for functions that never ends or always throw an exception.

typescript
function print(value: string): never { console.log(value); while (true) {} } function throwError(message: string): never { throw new Error(message); }

In TypeScript null and undefined are types that represent absence of a value. null and undefined can be used for all types, including primitive and object types.

typescript
// Using union type to allow null let age: number | null = null; age = 25; // OK age = null; // OK let address: string | undefined; address = "123 Main St"; // OK address = undefined; // OK

The behavior of these types depends on using strictNullChecks setting. It is recommended to turn strictNullChecks on for making code safer: when value can be null or undefined - you need to test for this value before accessing a variable or property.

typescript
interface User { id: number; name: string; } // A function that may return a User, null, or undefined function getUser(id: number): User | null | undefined { // ... Skip code for breviaty } const userID = 1; const user = getUser(userID); if (user === undefined) { console.log("Error fetching user data."); } else if (user === null) { console.log("User not found."); } else { console.log("User found: ${user.name}"); }

Summary

TypeScript has a rich type system that includes the following types:

  • Primitives: number, boolean, string
  • Enums
  • Objects
  • Functions
  • Arrays
  • Union Types, Intersection Types, Tuple Types
  • Special types: any, unknown, never, null, undefined

Hope you find this blog post useful. Happy coding!

Improve Your .NET and Architecture Skills

Join my community of 1800+ developers and architects.

Each week you will get 2 practical tips with best practises and architecture advice.