Answer a Question
Sign In

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

  • Do:

    declare and implement interfaces for code contracts

  • Avoid:

    defining large inline object types

  • Why:

    Interfaces reduce potential bugs/errors compared to using the any type

  • Why:

    Interfaces reduce code complexity when defining object types inline

  • Why:

    Interfaces are reusable throughout multiple TypeScript projects and libraries

Avoid

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

Do

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);
  }
}
Brian Love
Brian is a software engineer and Google Developer Expert in Web Technologies and Angular with a passion for learning, writing, speaking, teaching and mentoring. He regularly speaks at conferences and meetups around the country, and co-authored "Why Angular for the Enterprise" for O'Reilly. When not coding, Brian enjoys skiing, hiking, and being in the outdoors. Brian recently launched lookout.dev where you can find best practices and expert advice on topics ranging from TypeScript, Angular, React, Node.js and more.
Google Developers Expert

Whoa 🤚 You need to sign in to join the discussion.

Don't worry, if you start a comment, we'll save it for when you return. 😉