Patterns
Typescript Interfaces & Classes
The difference between Typescript interfaces and classes may seem subtle since both structures can be used to define the properties of an object. However, they usually serve different purposes within an angular application. This section is a quick guide on when to use each structure.
TL;DR
A good rule of thumb is to always start with an interface if you’re modeling data. You can always extend the interface later with a class if you need to add logic. Use a class when you know you’ll need extra logic or object instantiation.
Interfaces
Interfaces are used to describe the shape of an object and can be extended by other interfaces or classes. In general, interfaces should be used for all basic data representations. For example, you should use an interface to model data transfer objects (DTOs) when getting data from a back-end service. It is important to note that an interface cannot be used to generate new instances of an object the way that a class can be. They are only used by Typescript for type checking during development to enforce a certain data structure for objects.
export interface Gene {
symbol: string;
chr: string;
description: string;
}
getGenes(searchValue: string): Observable<Gene[]> {
return this.http.get<Gene[]>(`exampleapi.org/genes/?symbol=${searchValue}`);
}
In the example above, the http.get method populates an array of Gene objects. This makes it easier for developers to work with http responses in Angular.
Classes
Classes are basically blueprints for creating objects. Like an interface, a class can describe the shape of an object, but it can also set initial values and behaviors. Components and services are both common examples of classes specific to Angular. The following example shows how a class can be used to define what a Person object should look like as well as its associated logic.
class Person {
firstName: string;
lastName: string;
constructor(firstName: string, lastName: string) {
this.firstName = firstName;
this.lastName = lastName;
}
getFullName(): string {
return `${this.firstName} ${this.lastName}`
}
}
const person: Person = new Person('Spongebob', 'Squarepants');
console.log(‘Who lives in a pineapple under the sea?’);
console.log(person.getFullName());
If you’d like to read more on the subject, here are some good articles/documentation:
- Ultimate Courses - Classes vs Interfaces in TypeScript
- Medium - TypeScript Interfaces vs Classes: When to Use Each One
- TypeScript Handbook - Classes
- TypeScript Handbook - Object Types
Subscribing & Unsubscribing
Subscribing is the primary way of executing an Observable with RxJS. Some Observables are finite, meaning there is a set limit to the datastream that is subscribed to. For example, http requests result in a single response and then complete. Other Observables are infinite which means a subscription would keep listening for changes even after the component has been destroyed (e.g. DOM event listeners). This type of Observable needs to be unsubscribed from in order to avoid memory leaks. You do not need to worry about unsubscribing from finite Observables.
The following examples show different options for unsubscribing from an interval Observable that emits a number every second.
unsubscribe()
The most basic way of unsubscribing is the unsubscribe() method. This option involves assigning the subscription
to a variable (we'll go with mySub) and then calling mySub.unsubscribe() in the ngOnDestroy lifecycle hook. This ensures that
the subscription is stopped when the component is destroyed.
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class ExampleComponent implements OnInit, OnDestroy {
data$ = interval(1000);
mySub: Subscription;
ngOnInit(): void {
let mySub = this.data$.subscribe((data) => {
console.log(data);
});
}
ngOnDestroy(): void {
this.mySub.unsubscribe();
}
}
Async Pipe
If you don't need to do anything with the data after subscribing, the async pipe is the simplest unsubscribe option because it does all the work for you. The async pipe will subscribe to a given observable and then unsubscribe when the component is destroyed. The following example takes the same code from the first example and replaces all the subscribing and unsubscribing with async pipe in the component template.
Typescript
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class ExampleComponent {
data$ = interval(1000);
}
takeUntil(), takeWhile(), and take()
The preferred unsubscribe options are the various "take" operator functions. They may not be as straightforward as the async pipe, but they allow for more flexibility depending on your needs.
The most popular of these operators is takeUntil(). This option works similarly to the unsubscribe() method, but does
not require us to unsubscribe each individual subscription. Instead, you provide each Observable's takeUntil operator with
a notifier Subject that stops the subscription when triggered. The OnDestroy lifecycle hook is implemented to call the
subject's next() and complete() methods, which unsubscribes all subscriptions at once.
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class ExampleComponent implements OnInit, OnDestroy {
data$ = interval(1000);
slowerData$ = interval(5000);
unsubscribe$ = new Subject<void>();
ngOnInit(): void {
// Subscription 1
this.data$.pipe(takeUntil(this.unsubscribe$)).subscribe((data) => {
console.log(data);
});
// Subscription 2
this.slowerData$.pipe(takeUntil(this.unsubscribe$)).subscribe((data) => {
console.log(data);
});
}
ngOnDestroy(): void {
this.unsubscribe$.next();
this.unsubscribe$.complete();
}
}
The takeWhile() operator allows you to subscribe to an observable until a given condition is met. For example, you may
want to subscribe until the emitted value reaches some threshold.
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class ExampleComponent implements OnInit {
data$ = interval(1000);
ngOnInit(): void {
this.data$.pipe(takeWhile(value => value < 5)).subscribe((data) => {
console.log(data);
});
}
}
The take() operator allows you to take a set number of values from a stream. The following example takes only the first
value of the stream.
@Component({
selector: 'example',
templateUrl: './example.component.html',
})
export class ExampleComponent implements OnInit {
data$ = interval(1000);
ngOnInit(): void {
this.data$.pipe(take(1)).subscribe((data) => {
console.log(data);
});
}
}