Prefer Interfaces over the use of the any
type or large inline object types
Sunday, December 1, 2019
Prefer Interfaces over the use of the any
type or large inline object types
Interfaces in TypeScript (or any strongly typed language) provide a reusable type where a class is unnecessary. Interfaces should be generally preferred over large inline object types, and absolutely preferred over the use of the any
type. Most importantly, interfaces provide a code contract, both within your application, as well as with external systems. This is especially important when building for the web, which often communicates and relies upon external systems, resources, and application programming interfaces (API).
Defining an interface begins with the interface keyword followed by the name of the interface. Each member (property or method) signature is declared within the interface. Members can also be optional through the use of the question mark ( ? ) suffix. Here is an interface for a user:
interface User {
displayName: string;
email?: string;
dob: string | number | Date
}
A few things to note:
- The interface defines a new
User
type. - The
displayName
property is required and is astring
(and only a string). - The
email
property is optional because of the question mark ( ? ) suffix and is astring
. - The
dob
property is required and can be any of the following types: astring
,number
, orDate
object.
The interface above only declares property members of the object. An object can also include methods (or function), and we can use the interface to define the method signatures. This is important for specifying code contracts in TypeScript.
Let's look at an example of the OnInit
interface defined by Angular:
/**
* @description
* A lifecycle hook that is called after Angular has initialized
* all data-bound properties of a directive.
* Define an `ngOnInit()` method to handle any additional initialization tasks.
*/
export interface OnInit {
/**
* A callback method that is invoked immediately after the
* default change detector has checked the directive's
* data-bound properties for the first time,
* and before any of the view or content children have been checked.
* It is invoked only once when the directive is instantiated.
*/
ngOnInit(): void;
}
The primary purpose of the OnInit
interface is to establish a code contract: between your component and the framework. When the component class implements
the OnInit
interface we must specify the ngOnInit()
method (that must return void
).
Instructions
declare and implement interfaces for code contracts
defining large inline object types
Interfaces reduce potential bugs/errors compared to using the any
type
Interfaces reduce code complexity when defining object types inline
Interfaces are reusable throughout multiple TypeScript projects and libraries
Code Examples
Use interfaces to declare the props type
import React, { FunctionComponent } from "react";
import { User } from "../models";
interface Props {
onSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
user: User;
}
export const UserForm: FunctionComponent<Props> = ({
onSubmit,
user
}) => (
<form onsubmit={onSubmit}>
<input value={user?.displayName} />
<input value={user?.email} />
<input value={user?.dob} />
<button type="submit">Save</button>
</form>
);
Specify interfaces to ensure code contract
import { Component, EventEmitter, Input, Output, SimpleChanges } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
import { User } from '../models';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.css']
})
export class UserFormComponent {
formGroup = this.formBuilder.group({
displayName: ['', Validators.required],
email: '',
dob: ['', Validators.required]
});
@Input() user: User;
@Output() userChange = new EventEmitter<Partial<User>>();
constructor(private readonly formBuilder: FormBuilder) {}
ngOnChanges(simpleChanges: SimpleChanges) {
if (simpleChanges.user && simpleChanges.user.currentValue) {
this.formGroup.patchValue(this.user);
}
}
onSubmit(): void {
if (!this.formGroup.valid) {
return;
}
this.userChange.emit(this.formGroup.value);
}
}
do not use the any
type
import React, { FunctionComponent } from "react";
// NOTE: the use of the `any` type for the props
export const UserForm: FunctionComponent<any> = ({
onSubmit,
user
}) => (
<form onsubmit={onSubmit}>
<input value={user?.displayName} />
<input value={user?.email} />
<input value={user?.dob} />
<button type="submit">Save</button>
</form>
);
Do not infer the any
type
import { Component, EventEmitter, Input, Output } from '@angular/core';
import { FormBuilder, Validators } from '@angular/forms';
@Component({
selector: 'app-user-form',
templateUrl: './user-form.component.html',
styleUrls: ['./user-form.component.css']
})
export class UserFormComponent {
formGroup = this.formBuilder.group({
displayName: ['', Validators.required],
email: '',
dob: ['', Validators.required]
});
// NOTE: inferred `any` type.
@Input() user;
// NOTE: generic `any` type.
@Output() userChange = new EventEmitter<any>();
constructor(private readonly formBuilder: FormBuilder) {}
// NOTE: simpleChanges argument is inferred as `any`
ngOnChanges(simpleChanges) {
if (simpleChanges.user && simpleChanges.user.currentValue) {
this.formGroup.patchValue(this.user);
}
}
onSubmit(): void {
if (!this.formGroup.valid) {
return;
}
this.userChange.emit(this.formGroup.value);
}
}
Have a question or comment?