Understanding TypeScript Types

Chapter Outline

TypeScript Fundamentals: Understanding Types and Interfaces

TypeScript adds powerful features to JavaScript, enabling developers to write more reliable and maintainable code. One of the core features of TypeScript is its type system, which helps catch errors at compile time. In this article, we'll explore TypeScript's types and interfaces, understand their usage, and see how they can improve your development workflow. We'll also include code examples and tests using Jest to illustrate their practical application.

Basic Types

TypeScript provides several basic types that you can use to annotate your variables and function parameters.

Boolean

This basic data type expresses a simple true/false value.

typescript
let isDone: boolean = true;

Number

In JavaScript, as well as TypeScript, all numbers are either floating point values or BigIntegers. The floating point numbers are designated the type number, while BigIntegers are designated the type bigint. In addition to decimal and hexadecimal literals, TypeScript also supports binary and octal literals introduced in ECMAScript 2015.

typescript
1let decimal: number = 11;
2let hex: number = 0xf00d;
3let binary: number = 0b1011;
4let octal: number = 0o744;
5let bigNumber: bigint = 100n;

String

Text data in JavaScript is generally represented as data of type string. This is also the case in TypeScript.

typescript
1let color: string = "blue";
2color = 'red';
3let sentence: string = `Hello ${name}!`;

Array

JavaScript allows an array to be an ordered collection of values, of any type. However TypeScript allows enforcing the type of data that is contained within an array. Here are the ways an array may be declared in TypeScript:

typescript
1let list: number[] = [0, 1, 3];
2let evens: Array<number> = [2, 4, 6];

Tuple

Tuples are arrays with a fixed number of elements whose types are known. You can use tuples to express values such as the positions x, y, z in Cartesian coordinates, or longitude, latitude values representing geographic data.

typescript
1let x: [string, number];
2key = ["key", 1001]; // OK
3key = [10001, "key"]; // Error

Enum

Many high level languages such as C# and Java has had enumerated data types since their inception. JavaScript does not. TypeScript adds this feature which allows a more friendly way to assign names to a set of numeric values.

typescript
1enum Color {
2 Red,
3 Green,
4 Blue,
5}
6
7let c: Color = Color.Green;

It is also possible to manually set the values in the enum:

typescript
1enum Position {
2 First = 1,
3 Second = 2,
4 Third = 3,
5}
6
7let myPosition: Position = Position.Second;

The names of the enum values can be accessed by indexing into the enum as shown below:

typescript
1enum Color {
2 Red,
3 Green,
4 Blue,
5}
6
7let name: string = Color[2];
8// Displays 'Green'
9console.log(name);

Unknown

At times we may need to declare a variable whose type isn't known at compile time. For instance the value may come from dynamic content such as value returned from an API. In this instance, we want to instruct the compiler as well as future readers of the code that this variable could be anything, so we give it the unknown type.

typescript
1let notSure: unknown = 4;
2notSure = "maybe a string instead";
3
4// OK, definitely a boolean
5notSure = false;

unknown types cannot be directly assigned to another variable without performing a type check, such as a type guard:

typescript
1declare const maybe: unknown;
2// 'maybe' could be a string, object, boolean, undefined, or other types
3const aNumber: number = maybe;
4// Type 'unknown' is not assignable to type 'number'.
5
6if (maybe === true) {
7 // TypeScript knows that maybe is a boolean now
8 const aBoolean: boolean = maybe;
9 // So, it cannot be a string
10 const aString: string = maybe;
11 //Type 'boolean' is not assignable to type 'string'.
12}

Any

Sometimes you may need to opt-out of type checking all together, either when the type data is not available, or would take an inordinate amount of time to specify. In these scenarios, we label the variables with any type.

typescript
1declare function getValue(key: string): any;
2// OK, return value of 'getValue' is not checked
3const str: string = getValue("myString");

The any type is a powerful way to work with existing JavaScript source code, and gradually opt-into the TypeScript typing system.

Unlike unknown, variables of type any can access arbitrary properties, without any errors. This is similar to how JavaScript has been handling object properties.

All properties of an any type object are also of type any:

typescript
1let looselyTyped: any = {};
2let d = looselyTyped.a.b.c.d;
3// `d` is of type `any`

Null and Undefined

On their own, undefined and null are not terribly useful. However, null and undefined are subtypes of all other types. Therefore, you can assign null or undefined to a variable of any other type, such as a string.

However, when using the strictNullChecks flag in tsconfig.json, applications of null and undefined are limited. This helps avoid many common errors.

Never

The never type represents the type of values that never occur. As an example, never is the return type of a function that always throws an error, or never returns.

typescript
1// Function returning never must not have a reachable end point
2function error(message: string): never {
3 throw new Error(message);
4}
5
6// Inferred return type is never
7function fail() {
8 return error("Something failed");
9}
10
11// Function returning never must not have a reachable end point
12function infiniteLoop(): never {
13 while (true) {}
14}

Object

object is a type that represents composite types, i.e. anything that are not number, string, boolean, bigint, symbol, null, or undefined.

typescript
1declare function create(o: object | null): void;
2// OK
3create({ prop: 0 });
4// Argument of type 'number' is not assignable to parameter of type 'object'
5create(42)

About Number, String, Boolean, Symbol and Object

The types Number, String, Boolean, Symbol and Object are not equivalent to their lowercase versions. These types do not refer to the language primitives, and should almost never be used as types.

typescript
1function reverse(s: String): String {
2 return s.split("").reverse().join("");
3}
4
5reverse("hello world");

Type Annotations

You can explicitly specify the types of variables and function parameters to make your code more predictable and readable.

typescript
1function greet(name: string): string {
2 return `Hello, ${name}!`;
3}
4
5let greeting: string = greet('Alice');

Type assertions

Type assertions are a way to tell the compiler "trust me". 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 simply used by the compiler.

Assertions have two forms.

as-syntax:

typescript
1let someValue: unknown = "Hello World!";
2let strLen: number = (someValue as string).length;

"angle-bracket" syntax:

typescript
1let someValue: unknown = "Hello World!";
2let strLen: number = (<string>someValue).length;

When using TypeScript with JSX, onlyas-style assertions are allowed.

Interfaces

Interfaces are used to define the shape of objects, making your code more expressive and enforcing a contract for your objects.

typescript
1interface User {
2 id: number;
3 name: string;
4 email: string;
5}
6
7function getUser(): User {
8 return {
9 id: 1,
10 name: 'John Doe',
11 email: 'john.doe@example.com'
12 };
13}
14
15let user: User = getUser();

Type Aliases

Type aliases are similar to interfaces, but they can also represent primitive types, union types, and more.

typescript
1type ID = number;
2type Status = 'active' | 'inactive';
3
4interface Product {
5 id: ID;
6 name: string;
7 status: Status;
8}
9
10function getProduct(): Product {
11 return {
12 id: 101,
13 name: 'Laptop',
14 status: 'active'
15 };
16}
17
18let product: Product = getProduct();

Combining Types and Interfaces

You can combine types and interfaces to create more complex types.

typescript
1type Address = {
2 street: string;
3 city: string;
4 zipcode: string;
5};
6
7interface Customer {
8 id: ID;
9 name: string;
10 address: Address;
11}
12
13function getCustomer(): Customer {
14 return {
15 id: 202,
16 name: 'Jane Doe',
17 address: {
18 street: '123 Main St',
19 city: 'Somewhere',
20 zipcode: '12345'
21 }
22 };
23}
24
25let customer: Customer = getCustomer();

Type Guards

TypeScript type guards are functions or expressions that allow you to narrow down the type of a variable within a conditional block. They help the TypeScript compiler understand the specific type of a variable, enabling more precise type checking and preventing runtime errors.

Here are some common types of type guards and examples to illustrate their usage:

typeof Type Guards

The typeof operator can be used to narrow down types to primitive types such as string, number, boolean, and symbol.

typescript
1function printValue(value: string | number): void {
2 if (typeof value === "string") {
3 console.log(`String value: ${value}`);
4 } else if (typeof value === "number") {
5 console.log(`Number value: ${value}`);
6 }
7}
8
9printValue("Hello, World!"); // String value: Hello, World!
10printValue(42); // Number value: 42

instanceof Type Guards

The instanceof operator can be used to narrow down types to specific classes.

typescript
1class Dog {
2 bark() {
3 console.log("Woof!");
4 }
5}
6
7class Cat {
8 meow() {
9 console.log("Meow!");
10 }
11}
12
13function makeSound(animal: Dog | Cat): void {
14 if (animal instanceof Dog) {
15 animal.bark();
16 } else if (animal instanceof Cat) {
17 animal.meow();
18 }
19}
20
21const dog = new Dog();
22const cat = new Cat();
23
24makeSound(dog); // Woof!
25makeSound(cat); // Meow!

User-Defined Type Guards

User-defined type guards use a function to check whether a value is of a specific type. These functions return a boolean value and have a specific return type called a type predicate.

typescript
1interface Bird {
2 fly(): void;
3}
4
5interface Fish {
6 swim(): void;
7}
8
9function isBird(animal: Bird | Fish): animal is Bird {
10 return (animal as Bird).fly !== undefined;
11}
12
13function makeAnimalMove(animal: Bird | Fish): void {
14 if (isBird(animal)) {
15 animal.fly();
16 } else {
17 animal.swim();
18 }
19}
20
21const bird: Bird = {
22 fly: () => console.log("Flying")
23};
24
25const fish: Fish = {
26 swim: () => console.log("Swimming")
27};
28
29makeAnimalMove(bird); // Flying
30makeAnimalMove(fish); // Swimming

in Operator Type Guards

The in operator can be used to check whether an object has a specific property.

typescript
1interface Car {
2 drive(): void;
3}
4
5interface Boat {
6 sail(): void;
7}
8
9function moveVehicle(vehicle: Car | Boat): void {
10 if ("drive" in vehicle) {
11 vehicle.drive();
12 } else if ("sail" in vehicle) {
13 vehicle.sail();
14 }
15}
16
17const car: Car = {
18 drive: () => console.log("Driving")
19};
20
21const boat: Boat = {
22 sail: () => console.log("Sailing")
23};
24
25moveVehicle(car); // Driving
26moveVehicle(boat); // Sailing

Combining Type Guards

You can combine different type guards to handle complex type narrowing scenarios.

typescript
1interface Admin {
2 admin: true;
3 manage: () => void;
4}
5
6interface User {
7 admin: false;
8 view: () => void;
9}
10
11function isAdmin(person: Admin | User): person is Admin {
12 return person.admin === true;
13}
14
15function isUser(person: Admin | User): person is User {
16 return person.admin === false;
17}
18
19function handlePerson(person: Admin | User): void {
20 if (isAdmin(person)) {
21 person.manage();
22 } else if (isUser(person)) {
23 person.view();
24 }
25}
26
27const admin: Admin = {
28 admin: true,
29 manage: () => console.log("Managing")
30};
31
32const user: User = {
33 admin: false,
34 view: () => console.log("Viewing")
35};
36
37handlePerson(admin); // Managing
38handlePerson(user); // Viewing

Conclusion

Understanding types and interfaces is fundamental to mastering TypeScript. By defining explicit types and using interfaces to describe the shape of your objects, you can write more reliable and maintainable code. These practices also help catch errors early in the development process, improving your overall development workflow.

If you're new to TypeScript, I encourage you to start by annotating your variables and functions with types and gradually move on to defining interfaces for your objects. As you become more comfortable with TypeScript's type system, you'll find that it significantly enhances your productivity and code quality.

For further reading and in-depth tutorials, check out these resources:

Happy coding!

Feedback