Contents

Learn TypeScript: Interface & Type

You may want to read this article in Bahasa Indonesia version: Mengenal TypeScript: Interface & Type

Previously we have discussed the differences between Type and Interface in Learn TypeScript: Primitive Data Types, Type Aliases, and Interfaces, but this time we will discuss in more detail about three important aspects of Type and Interface: Function Interface, Extending Interface, and Properties of Data Type Function in Interface/Type.

Function Interface

Interfaces in TypeScript can not only be used to define object shapes, but also to define function shapes. This is called a function interface.

Here’s how to define a function interface in TypeScript.

/** Defining function interface */
interface MathOperation {
  (x: number, y: number): number;
}

/** Implementing function interface */
const add: MathOperation = (a, b) => a + b;
const subtract: MathOperation = (a, b) => a - b;

console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

In the example above, MathOperation is a function interface that defines a function with two parameters of type number and returns a value of type number.

Function interfaces can also have additional properties:

interface FetchFunction {
  (url: string, options?: object): Promise<Response>;
  timeout: number;
  cache?: boolean;
}

const fetchData: FetchFunction = async (url, options) => {
  // Fetch implementation
  return new Response();
};

// Adding properties to the function
fetchData.timeout = 3000;
fetchData.cache = true;

console.log(fetchData.timeout); // 3000

Function interfaces can also be created using type aliases:

/** Using interface */
interface Logger {
  (message: string): void;
}

/** Using type alias */
type LoggerType = (message: string) => void;

/** Both can be used in the same way */
const consoleLogger: Logger = (msg) => console.log(msg);
const fileLogger: LoggerType = (msg) => {
  // File logging implementation
};

Extending Interface

One of the strengths of interfaces in TypeScript is their ability to be extended or inherited. This allows us to create new interfaces based on existing ones.

This is the simplest example when you want to extend an Interface.

/** Basic interface */
interface Person {
  name: string;
  age: number;
}

/** Interface extending Person */
interface Employee extends Person {
  employeeId: string;
  department: string;
}

/** Object must have all properties from Person and Employee */
const employee: Employee = {
  name: "Budi",
  age: 30,
  employeeId: "E001",
  department: "Engineering"
};

Interfaces can also extend more than one interface:

interface HasName {
  name: string;
}

interface HasAge {
  age: number;
}

interface HasAddress {
  address: string;
}

/** Extending multiple interfaces */
interface Contact extends HasName, HasAge, HasAddress {
  email: string;
  phone: string;
}

const contact: Contact = {
  name: "Ani",
  age: 25,
  address: "123 Freedom St.",
  email: "ani@example.com",
  phone: "08123456789"
};

Interfaces can also be extended using generics:

interface BaseResponse<T> {
  status: number;
  message: string;
  timestamp: Date;
}

interface DataResponse<T> extends BaseResponse<T> {
  data: T;
}

/** Using interface with generics */
const userResponse: DataResponse<{ id: number; username: string }> = {
  status: 200,
  message: "Success",
  timestamp: new Date(),
  data: {
    id: 1,
    username: "johndoe"
  }
};

A more in-depth explanation of the concept of Generics in TypeScript can be read here: Learn TypeScript: Generics

Function-typed Properties in Interfaces/Types

Interfaces and type aliases in TypeScript can have properties of function type. This is very useful for defining objects with methods or callbacks.

/** Interface with methods */
interface Calculator {
  add(a: number, b: number): number;
  subtract(a: number, b: number): number;
  multiply?(a: number, b: number): number; // Optional method
}

/** Interface implementation */
const simpleCalc: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b
};

const advancedCalc: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b
};

console.log(simpleCalc.add(5, 3));        // 8
console.log(advancedCalc.multiply?.(5, 3)); // 15

Interfaces can define function-typed properties with explicit signatures, clearly specifying parameters and return types. This approach enhances type safety and makes the interface contract clearer for developers implementing it.

The difference from methods in interfaces is in syntax and usage:

Methods use the syntax methodName(params): returnType, while function properties use the syntax propertyName: (params) => returnType.

Methods emphasize that the function is part of the object’s behavior, while function properties treat the function as data stored in a property.

Methods are easier to override in an inheritance context, while function properties emphasize a more functional approach.

interface EventHandler {
  /** Function property with explicit signature */
  onClick: (event: MouseEvent) => void;
  onHover: (event: MouseEvent) => boolean;
  onSubmit(formData: object): Promise<Response>;
}

const handler: EventHandler = {
  onClick: (event) => {
    console.log("Clicked", event.target);
  },
  onHover: (event) => {
    console.log("Hover", event.target);
    return true;
  },
  onSubmit: async (formData) => {
    // Submit implementation
    return new Response();
  }
};

We can also use generics when declaring a function property on an interface:

interface DataProcessor<T, R> {
  process: (data: T) => R;
  validate: (data: T) => boolean;
}

// Implementation for string and number
const stringToNumber: DataProcessor<string, number> = {
  process: (data) => parseInt(data, 10),
  validate: (data) => !isNaN(Number(data))
};

console.log(stringToNumber.process("42"));    // 42
console.log(stringToNumber.validate("42"));   // true
console.log(stringToNumber.validate("abc"));  // false

A more in-depth explanation of the concept of Generics in TypeScript can be read here: Learn TypeScript: Generics

Function properties are very useful for defining callback patterns:

interface FetchOptions {
  url: string;
  method: "GET" | "POST" | "PUT" | "DELETE";
  headers?: Record<string, string>;
  body?: object;
  onSuccess: (data: any) => void;
  onError: (error: Error) => void;
  onComplete?: () => void;
}

function fetchWithCallbacks(options: FetchOptions): void {
  try {
    // Fetch simulation
    const data = { result: "success" };
    options.onSuccess(data);
  } catch (error) {
    options.onError(error as Error);
  } finally {
    options.onComplete?.();
  }
}

// Using function with callbacks
fetchWithCallbacks({
  url: "https://api.example.com/data",
  method: "GET",
  onSuccess: (data) => console.log("Data:", data),
  onError: (error) => console.error("Error:", error),
  onComplete: () => console.log("Request completed")
})

The code example above illustrates the implementation of the callback pattern in TypeScript using interfaces. The FetchOptions interface defines a structure for making HTTP requests with callback functions.

In this example:

  • onSuccess is a required callback that is called when the request succeeds
  • onError is a required callback for handling errors
  • onComplete is an optional callback that is called after the request completes (whether successful or not)

The fetchWithCallbacks function accepts an object that conforms to the FetchOptions interface and runs the appropriate callback based on the operation result. This pattern is very useful for asynchronous operations because it allows the calling code to specify what to do with the operation result without having to wait for the operation to complete.

The use of optional chaining (?.) on options.onComplete?.() demonstrates TypeScript’s advantage in safely handling optional callbacks.

TypeScript provides two ways to define methods in interfaces:

interface User {
  /** Method syntax */
  getFullName(): string;
  
  /** Function property syntax */
  calculateAge: (birthYear: number) => number;
}

const user: User = {
  getFullName() {
    return "John Doe";
  },
  calculateAge: (birthYear) => new Date().getFullYear() - birthYear
};

console.log(user.getFullName());       // "John Doe"
console.log(user.calculateAge(1990));  // 35 (in 2025)

These two approaches are functionally identical, but method syntax is more concise and more similar to class syntax in JavaScript.

Conclusion

Interfaces and Types in TypeScript offer flexible and powerful ways to define data shapes in our applications. Function interfaces allow us to define contracts for functions, extending interfaces allow us to create structured type hierarchies, and function-typed properties give us the ability to define objects with behavior.

By understanding these three concepts, we can create TypeScript code that is more expressive, reusable, and type-safe. The functional programming approach with TypeScript becomes easier when we can clearly define function contracts and leverage function-typed properties to create more modular code.

Related Content