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.
Defining 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 Interface dengan Property
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 Interface vs Type Alias
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.
Basic Interface Extension
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"
};
Multiple Interface Extension
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"
};
Extending Interface with Generics
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.
Methods in Interfaces
/** 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
Function Properties with Explicit Signatures
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:
Syntax
Methods use the syntax methodName(params): returnType
, while function properties use the syntax propertyName: (params) => returnType
.
Semantics
Methods emphasize that the function is part of the object’s behavior, while function properties treat the function as data stored in a property.
Usage in inheritance context
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();
}
};
Function Properties with Generics
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
Callback Pattern
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 succeedsonError
is a required callback for handling errorsonComplete
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.
Function Property vs Method Syntax
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.