lookout.devlookout.dev
search
Share Knowledge
00

Prefer Interfaces over the use of the any type or large inline object types

Sunday, December 1, 2019

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 a string (and only a string).
  • The email property is optional because of the question mark ( ? ) suffix and is a string.
  • The dob property is required and can be any of the following types: a string, number, or Date 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

checkmark-circle
Do

declare and implement interfaces for code contracts

error-circle
Avoid

defining large inline object types

info-circle
Why

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);
  }
}
Brian Love

I am a software engineer and Google Developer Expert in Web Technologies and Angular with a passion for learning, writing, speaking, teaching and mentoring. I regularly speaks at conferences and meetups around the country, and co-authored "Why Angular for the Enterprise" for O'Reilly. When not coding, I enjoy skiing, hiking, and being in the outdoors. I started lookout.dev to break down the barriers of learning in public. Learning in public fosters growth - for ourselves and others.

Google Developers Expert

Have a question or comment?