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.2boolean
- represents atrue
offalse
valuestring
- represents text values, like "Some text here"
Here is how to define variables of primitive types:
typescriptconst total: number = 100; const isActive: boolean = true; const userName: string = "Anton";
Alternatively we can define variables like this, where types will be inferred automatically:
typescriptconst 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.
typescriptenum 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.
typescriptenum 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:
typescripttype User = { id: number; name: string; }; const user: User = { id: 1, name: "Anton" };
Or interface
keyword:
typescriptinterface 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.
typescriptfunction 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:
typescriptconst 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:
typescriptconst 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:
typescriptconst value: any = "some text here"; const length: number = (value as string).length;
Alternatively you can use <type>
syntax:
typescriptconst 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:
typescriptlet 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:
typescriptfunction 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:
typescriptfunction 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:
typescriptlet 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:
typescriptfunction 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:
typescriptinterface 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.
typescriptconst 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:
typescriptfunction 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:
typescriptlet 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:
typescriptlet 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.
typescriptfunction 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.
typescriptinterface 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!