TypeScript Interface with Functions: 6 Patterns You Need to Know

TypeScript interfaces can absolutely have functions, and there are several useful patterns beyond the basic “add a method and implement it in a class.” In this tutorial, I’ll walk you through those patterns in a practical, no-nonsense way.

Why put functions in a TypeScript interface?

When I say “functions in an interface,” I’m really talking about two things:

  • Methods on an object or class (like getFullName() on a Person)
  • Function-shaped members (like a formatter callback or a callable type)

I like to think of an interface as a promise: “anything that claims to be this type will have these properties and these functions.” The more clearly you express that promise, the easier your code is to read, test, and refactor.

All examples use US-style names and scenarios so it feels familiar if you’re building apps for a typical US audience.

What is a method signature in a TypeScript interface?

method signature in an interface is just a function declaration without any implementation.

For example:

interface Person {
firstName: string;
lastName: string;
getFullName(): string;
}

Here’s what this says:

  • Every Person must have firstName and lastName as strings.
  • Every Person must have a getFullName function that returns a string.
  • The interface doesn’t care how getFullName is implemented, only that it exists and returns a string.

I like to think of this as the “API” for your object. The interface is the contract; the class (or object literal) is the implementation.

Pattern 1: Basic method signature in an interface

Let’s start with a simple example where we define a Person interface and a class that implements it.

interface Person {
firstName: string;
lastName: string;
age: number;
getFullName(): string;
}

class AmericanPerson implements Person {
firstName: string;
lastName: string;
age: number;

constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}
}

const johnDoe = new AmericanPerson("John", "Doe", 30);
console.log(johnDoe.getFullName()); // John Doe

You can see the output in the screenshot below.

TypeScript Interface with Functions

A few practical points:

  • The interface defines what needs to exist.
  • The class uses implements to promise that it will match the interface.
  • If you forget to implement getFullName, TypeScript will complain right away.

This pattern is great for domain models, service contracts, and places where you expect multiple implementations of the same shape.

Pattern 2: Interface methods with parameters and return types

Next, let’s add a method that takes parameters and returns a value.

Imagine we want a method that calculates a person’s birth year:

interface Person {
firstName: string;
lastName: string;
age: number;
getFullName(): string;
getBirthYear(currentYear: number): number;
}

class AmericanPerson implements Person {
firstName: string;
lastName: string;
age: number;

constructor(firstName: string, lastName: string, age: number) {
this.firstName = firstName;
this.lastName = lastName;
this.age = age;
}

getFullName(): string {
return `${this.firstName} ${this.lastName}`;
}

getBirthYear(currentYear: number): number {
return currentYear - this.age;
}
}

const janeSmith = new AmericanPerson("Jane", "Smith", 25);
console.log(janeSmith.getBirthYear(2024)); // 1999

You can see the output in the screenshot below.

Interface with Functions TypeScript

A few habits I recommend:

  • Always type both the parameter list and the return type in the interface.
  • Don’t rely on “it can be inferred” here — this is the contract, so be explicit.
  • Keep method names descriptive; future-you will thank you.

Pattern 3: Optional methods in an interface

Sometimes you want to say “this method may exist, but it’s not required.” That’s where optional methods come in.

Here’s a logger example with an optional logWarning:

interface Logger {
logInfo(message: string): void;
logError(message: string): void;
logWarning?(message: string): void; // optional method
}

class ConsoleLogger implements Logger {
logInfo(message: string): void {
console.log(`Info: ${message}`);
}

logError(message: string): void {
console.error(`Error: ${message}`);
}

// logWarning is optional, so we don't have to implement it
}

const logger = new ConsoleLogger();
logger.logInfo("Application started");
logger.logError("Failed to connect to database");

You can see the output in the screenshot below.

Interface with Functions in TypeScript

Where I use this pattern in real projects:

  • Plugin systems or extension points where not every implementation needs every method
  • Feature flags or experimental methods that only some implementations support
  • Interfaces shared across multiple services where some behavior is truly optional

When you call an optional method, you should always null-check it:

logger.logWarning?.("Low disk space");

That way, you only call it if it exists.

Pattern 4: Function properties vs. method signatures

This is one area where a lot of developers get confused. There are two ways to express a function in an interface:

Method signature:

interface FormatterWithMethod {
format(value: string): string; // method
}

Function property:

interface FormatterWithProperty {
format: (value: string) => string; // function property
}

They look almost the same, but they’re not identical.

Let’s see both in practice:

// Method signature - usually used with classes
interface FormatterWithMethod {
format(value: string): string;
}

class UpperCaseFormatter implements FormatterWithMethod {
format(value: string): string {
return value.toUpperCase();
}
}

const methodFormatter = new UpperCaseFormatter();
console.log(methodFormatter.format("hello")); // HELLO


// Function property - common in config objects, React props, etc.
interface FormatterWithProperty {
format: (value: string) => string;
}

const propertyFormatter: FormatterWithProperty = {
format: (value) => value.toLowerCase(),
};

console.log(propertyFormatter.format("HELLO")); // hello

When I use each:

  • Method signatures: when I expect a Typescript class to implement the interface.
  • Function properties: when I’m passing around plain objects, especially for callbacks (React props, configuration, event handlers, etc.).

If you’re building something like a config object or options bag, the function property style is usually more natural.

Pattern 5: Async methods in an interface

Async work is everywhere now, so it’s very common to have interface methods that return a Promise.

Here’s a simple data service example:

interface User {
id: string;
name: string;
email: string;
}

interface UserService {
fetchUser(userId: string): Promise<User>;
saveUser(user: User): Promise<void>;
}

class ApiUserService implements UserService {
async fetchUser(userId: string): Promise<User> {
const response = await fetch(`/api/users/${userId}`);
if (!response.ok) {
throw new Error("Failed to fetch user");
}
return response.json();
}

async saveUser(user: User): Promise<void> {
const response = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(user),
});

if (!response.ok) {
throw new Error("Failed to save user");
}
}
}

Key details:

  • The interface uses Promise<T> in the Typescript return type.
  • The async keyword is only used in the implementation, not in the interface.
  • This pattern works nicely with dependency injection and unit testing.

A simple test double for this interface might look like:

class InMemoryUserService implements UserService {
private users = new Map<string, User>();

async fetchUser(userId: string): Promise<User> {
const user = this.users.get(userId);
if (!user) {
throw new Error("User not found");
}
return user;
}

async saveUser(user: User): Promise<void> {
this.users.set(user.id, user);
}
}

Pattern 6: Extending interfaces to add more methods

You don’t have to put everything into a single interface from day one. I prefer to build smaller, Begin with focused interfaces and extend interfaces only when necessary.

Here’s an example with animals and pets:

interface Animal {
name: string;
speak(): string;
}

interface Pet extends Animal {
owner: string;
fetch(item: string): string;
}

class Dog implements Pet {
name: string;
owner: string;

constructor(name: string, owner: string) {
this.name = name;
this.owner = owner;
}

speak(): string {
return `${this.name} says: Woof!`;
}

fetch(item: string): string {
return `${this.name} fetched the ${item}!`;
}
}

const buddy = new Dog("Buddy", "Emily Johnson");
console.log(buddy.speak()); // Buddy says: Woof!
console.log(buddy.fetch("ball")); // Buddy fetched the ball!

Why I like this pattern:

  • You can keep base interfaces small and composable.
  • You can define specialized interfaces for specific scenarios (e.g., AdminUser extends User).
  • It plays nicely with existing libraries. For example, you can extend a third-party interface with your own methods in your app.

Callable interfaces (function types with extra properties)

One more pattern that’s really handy: sometimes you want something that is both a function and an object with properties.

That’s where callable interfaces (call signatures) come in:

interface StringTransformer {
(value: string): string; // call signature
description: string; // extra property
}

const exclaim: StringTransformer = (value: string) => {
return `${value}!`;
};

exclaim.description = "Adds an exclamation mark to the end of the string";

console.log(exclaim("Hello")); // Hello!
console.log(exclaim.description); // Adds an exclamation mark...

Where this shows up:

  • Middleware functions that also have configuration attached
  • Utility functions that carry metadata
  • Libraries that expose a function but attach helper properties

You can define these with interfaces or type aliases; both work. I tend to use whatever keeps the code clearer in that file.

When to use interface methods vs. abstract classes

You might be wondering: “Should this be an interface or an abstract class?” I ask myself a few simple questions.

I reach for an interface when:

  • I care about the shape of the object, not the base behavior.
  • I want multiple unrelated classes (or objects) to implement the same contract.
  • I’m defining a public API surface or a shared type.

I reach for an abstract class when:

  • I want to provide some default implementation.
  • I need to share common logic or state across subclasses.
  • I want to enforce the use of certain methods and also give partial implementations.

Here’s a tiny side-by-side:

interface PaymentProcessor {
process(amount: number): Promise<void>;
}

// any class can implement this, with completely custom behavior


abstract class BasePaymentProcessor {
async process(amount: number): Promise<void> {
this.validateAmount(amount);
await this.execute(amount);
}

protected validateAmount(amount: number): void {
if (amount <= 0) {
throw new Error("Amount must be positive");
}
}

protected abstract execute(amount: number): Promise<void>;
}

class StripePaymentProcessor extends BasePaymentProcessor {
protected async execute(amount: number): Promise<void> {
console.log(`Charging $${amount} via Stripe`);
}
}

In many real-world apps, I’ll use interfaces for external contracts (things my code exposes to others) and abstract classes for internal hierarchies.

Common mistakes when adding functions to interfaces

I see the same issues over and over in code reviews. Here are a few to avoid.

1. Putting implementation details in the interface

Bad:

interface UserService {
async fetchUser(id: string): Promise<User>; // async here is pointless
}

Better:

interface UserService {
fetchUser(id: string): Promise<User>;
}

The async keyword belongs in the class, not in the interface.

2. Forgetting to type the return value

Bad:

interface Calculator {
add(a: number, b: number);
}

TypeScript will infer any if noImplicitAny is off, which can bite you later.

Better:

interface Calculator {
add(a: number, b: number): number;
}

3. Mixing up method signatures and function properties

If you copy/paste between:

interface Example {
run(): void;
}

and

interface Example {
run: () => void;
}

…be clear about what you want:

  • Method signature: ideal for classes.
  • Function property: ideal for plain objects and configs.

4. Overloading interfaces with too many responsibilities

If your interface has a dozen unrelated methods, it’s probably time to split it.

Instead of:

interface UserManager {
createUser(): void;
deleteUser(): void;
resetPassword(): void;
sendNewsletter(): void;
exportUsersToCsv(): void;
}

Consider:

interface UserAdmin {
createUser(): void;
deleteUser(): void;
resetPassword(): void;
}

interface UserReporter {
exportUsersToCsv(): void;
}

interface UserCommunicator {
sendNewsletter(): void;
}

Smaller interfaces are easier to test, implement, and change.

Putting it all together

Let’s pull these ideas into one slightly more realistic example.

Imagine we’re building a small user management module:

interface User {
id: string;
name: string;
email: string;
}

interface UserFormatter {
format(user: User): string;
}

interface UserNotifier {
sendWelcomeEmail?(user: User): Promise<void>; // optional
}

interface UserRepository {
fetchById(id: string): Promise<User>;
save(user: User): Promise<void>;
}

class SimpleUserFormatter implements UserFormatter {
format(user: User): string {
return `${user.name} <${user.email}>`;
}
}

class InMemoryUserRepository implements UserRepository {
private users = new Map<string, User>();

async fetchById(id: string): Promise<User> {
const user = this.users.get(id);
if (!user) {
throw new Error("User not found");
}
return user;
}

async save(user: User): Promise<void> {
this.users.set(user.id, user);
}
}

class EmailUserNotifier implements UserNotifier {
async sendWelcomeEmail(user: User): Promise<void> {
console.log(`Sending welcome email to ${user.email}`);
}
}

// Using the interfaces together
async function createAndGreetUser(
repo: UserRepository,
formatter: UserFormatter,
notifier: UserNotifier,
) {
const newUser: User = {
id: "u123",
name: "Michael Brown",
email: "michael.brown@example.com",
};

await repo.save(newUser);

const message = formatter.format(newUser);
console.log("Created user:", message);

await notifier.sendWelcomeEmail?.(newUser);
}

const repo = new InMemoryUserRepository();
const formatter = new SimpleUserFormatter();
const notifier = new EmailUserNotifier();

createAndGreetUser(repo, formatter, notifier);

This might look like “a lot of interfaces” for a small example, but this is exactly the sort of structure that pays off as your app grows. Each interface:

  • Describes a clear contract
  • Has functions with well-defined types
  • Can be implemented, mocked, or swapped independently

That’s the real power of putting functions in interfaces.

You may also read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.