Skip to content

Defining Types

A “type” is a category of an acceptable domain of values. We know, for example, that when we declare a variable of type string, it can only hold values that are strings, such as "hello" or "world".

That is a pretty broad type, because it can hold any string value. But we can also define narrower types that only allow certain values or structures.

For example, if we wanted a string-like type that only allows the values "red", "green", or "blue", we could define a type like this:

type Color = "red" | "green" | "blue";
let myColor: Color;
myColor = "red"; // OK
myColor = "yellow"; // Error: Type '"yellow"' is not assignable to type 'Color'

This is us saying that anything of type Color is a subset of some literal strings that define the permissable values. The type keyword is used to define a new type (called an alias), and the | operator is used to create a union type that allows any of the specified values.

So we are saying that values of type Color can only be one of the three specified strings. If we try to assign a value that is not one of those, TypeScript will give us an error.

A nice side-effect of this is that not only is the TypeScript compiler able to check that we are using the correct values, but it can also provide autocomplete suggestions in our editor. So if we start typing myColor = ", the editor will suggest "red", "green", and "blue" as possible completions.

In TypeScript, you can define the structure of an object using an interface or a type alias. This allows you to specify what properties an object should have and their types.

interface Person {
name: string;
age: number;
isEmployed: boolean;
}
type Car = {
make: string;
model: string;
year: number;
};
const person: Person = {
name: "Alice",
age: 30,
isEmployed: true
};
const car: Car = {
make: "Toyota",
model: "Camry",
year: 2020
};

You can also use an interface to define a type:

interface Point {
x: number;
y: number;
}
const point: Point = {
x: 10,
y: 20
};
type Person = {
name: string;
age: number;
};
type Employee = Person & {
employeeId: number;
department: string;
salary:number;
};
const employee: Employee = {
name: "Alice",
age: 30,
employeeId: 12345,
department: "Engineering",
salary: 75000
};

A discriminated union is a powerful way to define a type that can be one of several different types, each with its own properties. You can use a common property (called a “discriminator”) to determine which type it is.

type Person = {
name: string;
age: number;
};
type Employee = Person & {
kind: "employee";
employeeId: number;
department: string;
salary:number;
};
type Customer = Person & {
kind: "customer";
customerId: number;
purchaseHistory: string[];
};
type User = Employee | Customer;
const user1: Employee = {
name: "Alice",
age: 30,
employeeId: 12345,
department: "Engineering",
salary: 75000
};
const user2: Customer = {
name: "Bob",
age: 25,
customerId: 67890,
purchaseHistory: ["item1", "item2"]
};
function handleUser(user: User) {
if (user.kind === "employee") {
console.log(`Employee: ${user.name}, Department: ${user.department}`);
} else {
console.log(`Customer: ${user.name}, Purchases: ${user.purchaseHistory.join(", ")}`);
}
}
handleUser(user1); // Employee: Alice, Department: Engineering
handleUser(user2); // Customer: Bob, Purchases: item1, item2

You can also define types on the fly without giving them a name. This is useful for one-off types that you don’t need to reuse elsewhere.

const person: { name: string; age: number; isEmployed: boolean, email: string } = {
name: "Alice",
age: 30,
isEmployed: true
email: 'Alice@aol.com'
};
function sendMessage(to: { name: string; email: string }, message: string) {
console.log(`Sending message to ${to.name} at ${to.email}: ${message}`);
}
sendMessage({ name: "Bob", email: "bob@example.com" }, "Hello Bob!");
sendMessage(person, "Hello Alice!");

This is an example of structural typing, where TypeScript checks that the object has the required properties and types, even if it is not explicitly defined as a type or interface.

TypeScript has great support for defining types with classes. Way more than I’ll show here, actually, because we don’t tend to use a lot of this style of coding in Angular.

TypeScript has an affordance where you can declare fields in a class constructor, and TypeScript will automatically create the corresponding properties on the class instance. This is a nice shorthand that saves you from having to declare the properties separately.

However, it is just a TypeScript “thing”. If you are using TypeScript to produce JavaScript that others will use (without TypeScript), there is no protection for those private properties.

class Person {
constructor(public name:string, private age:number) {}
getInfo() {
return `Person ${this.name} is ${this.age}`;
}
}

Produces the following JavaScript code:

class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
getInfo() {
return `Person ${this.name} is ${this.age}`;
}
}

JavaScript also does not have field scoping modifiers.

public is the default, and so isn’t usually specified.

If you use private, the TypeScript compiler may actually emit JavaScript code to simulate private properties.

For example, with this code:

class BankAccount {
private balance: number;
constructor() {
this.balance = 0;
}
public deposit(amount:number) {
this.balance += amount;
}
public withdraw(amount: number) {
this.balance -= amount;
}
getBalance() {
return this.balance;
}
}
const myAccount = new BankAccount();
myAccount.deposit(125.23);
myAccount.withdraw(100.00);
console.log(myAccount.getBalance());
myAccount.balance = 5000;
console.log(myAccount.getBalance())
}

You shouldn’t be able to access the balance property directly, but you can. The TypeScript compiler will show an error, but the JavaScript code will still allow it.

In 2022, JavaScript introduced a new way to define private properties using the # syntax. This is not TypeScript specific, but it is worth noting because it is now part of the JavaScript language.

class BankAccount {
#balance: number;
constructor() {
this.#balance = 0;
}
public Deposit(amount:number) {
this.#balance += amount;
}
public withdraw(amount: number) {
this.#balance -= amount;
}
getBalance() {
return this.#balance;
}
}
const myAccount = new BankAccount();
myAccount.Deposit(125.23);
myAccount.withdraw(100.00);
console.log(myAccount.getBalance());
// myAccount.#balance = 5000;
console.log(myAccount.getBalance())

This will produce a JavaScript error if you try to access the #balance property directly, as it is now truly private to the class.

As a matter of fact the TypeScript compiler, if you set the target to equal to earlier than ES2022, it will emit JavaScript code that uses a “wrapper” to protect these properties.

That same code above (for the class) will emit the following JavaScript:

"use strict";
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, privateMap, value) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to set private field on non-instance");
}
privateMap.set(receiver, value);
return value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, privateMap) {
if (!privateMap.has(receiver)) {
throw new TypeError("attempted to get private field on non-instance");
}
return privateMap.get(receiver);
};
var _balance;
class BankAccount {
constructor() {
_balance.set(this, void 0);
__classPrivateFieldSet(this, _balance, 0);
}
Deposit(amount) {
__classPrivateFieldSet(this, _balance, __classPrivateFieldGet(this, _balance) + amount);
}
withdraw(amount) {
__classPrivateFieldSet(this, _balance, __classPrivateFieldGet(this, _balance) - amount);
}
getBalance() {
return __classPrivateFieldGet(this, _balance);
}
}

See Private Properties for more information on this new feature.

You can also use interfaces to define the structure of a class. This is useful for ensuring that a class adheres to a specific contract.

it('example 2 - class with inteface', () => {
interface ProvidesInformation {
getInfo(): string;
}
class Employee implements ProvidesInformation {
name: string;
position: string;
constructor(name: string, position: string) {
this.name = name;
this.position = position;
}
getInfo() {
return `${this.name} is a ${this.position}.`;
}
}
class Product implements ProvidesInformation {
getInfo(): string {
return 'This is a product.';
}
}
const employee = new Employee('Bob', 'Software Engineer');
const product = new Product();
function displayInfo(item: ProvidesInformation) {
return item.getInfo();
}
expect(displayInfo(employee)).toBe('Bob is a Software Engineer.');
expect(displayInfo(product)).toBe('This is a product.');
});

However, keep in mind that TypeScript does not enforce the implementation of interfaces at runtime.

It is structurally typed, meaning that as long as the class has the required properties and methods, it will satisfy the interface.

The following code will work just as well:

it('example 2 - class with inteface', () => {
interface ProvidesInformation {
getInfo(): string;
}
class Employee {
name: string;
position: string;
constructor(name: string, position: string) {
this.name = name;
this.position = position;
}
getInfo() {
return `${this.name} is a ${this.position}.`;
}
}
class Product {
getInfo(): string {
return 'This is a product.';
}
}
const employee = new Employee('Bob', 'Software Engineer');
const product = new Product();
function displayInfo(item: ProvidesInformation) {
return item.getInfo();
}
expect(displayInfo(employee)).toBe('Bob is a Software Engineer.');
expect(displayInfo(product)).toBe('This is a product.');
});
});