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 :
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:
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.
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.
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:
type User = {
id: number;
name: string;
};
const user: User = { id: 1, name: "Anton" };
Or interface keyword:
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.
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:
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 for creating arrays:
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:
const value: any = "some text here";
const length: number = (value as string).length;
Alternatively you can use syntax:
const value: any = "some text here";
const length: number = (<string>value).length;
Important tip: you can’t use 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:
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:
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:
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:
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:
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:
interface Person {
name: string;
}
interface ContactDetails {
email: string;
phone: string;
}
type Customer = Person & ContactDetails;
const customer: Customer = {
name: "Anton",
email: "anton@devtips.com",
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.
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:
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 :
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:
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:
- TypeScript: The Danger of Using the Any Keyword
- TypeScript: Any vs Unknown — Understanding the Difference
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.
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.
// 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.
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!
Originally published at https://antondevtips.com.
After reading the post consider the following:
- Subscribe to receive newsletters with the latest blog posts
- Download the source code for this post from my github (available for my sponsors on BuyMeACoffee and Patreon)
If you like my content — consider supporting me
Unlock exclusive access to the source code from the blog posts by joining my Patreon and Buy Me A Coffee communities!
Top comments (0)