TypeScript Types: Why Do I Have to Spell It Out Twice?

Conrad Mugabe
3 min readSep 15, 2024

--

Photo by Julia Taubitz on Unsplash

If you’ve been using TypeScript for a while, you’re probably familiar with its powerful type-checking features that help catch errors during development. Types, interfaces, and enums are crucial to shaping the structure of your code and ensuring it’s robust. However, you may have stumbled upon a little inconsistency when dealing with class vs. function implementations that can make you question, “Why doesn’t TypeScript infer my types automatically here?”

Let me explain.

The Setup: Types in TypeScript

Let’s consider a basic setup for a ticketing system.

enum TicketStatus {
OPEN = "OPEN",
IN_PROGRESS = "IN_PROGRESS",
CLOSED = "CLOSED",
}

interface CreateTicketRequest {
title: string;
description: number;
}

interface CreateTicketResponse {
id: string;
title: string;
description: string;
status: TicketStatus;
createdAt: Date;
updatedAt: Date;
}

interface DatabaseService {
createTicket(data: CreateTicketRequest): Promise<CreateTicketResponse>;
}

We’ve defined an interface for the ticket creation request (CreateTicketRequest) and response (CreateTicketResponse), along with the DatabaseService interface that declares the createTicket method.

The Class Implementation: Explicit Typing

Let’s implement this service using a class.

class MongoDatabase implements DatabaseService {
async createTicket(data: CreateTicketRequest): Promise<CreateTicketResponse> {
// Implementation here
}
}

In the class-based implementation, we explicitly state the type of data as CreateTicketRequest and the return type as Promise<CreateTicketResponse>. This is because TypeScript requires us to honor the contract defined by the DatabaseService interface.

The Function Implementation: Implicit Typing

Now, let’s look at the same service, implemented as a function.

function MongoDatabase(): DatabaseService {
return {
async createTicket(data) {
// Implementation here
}
}
}

Surprisingly, in the function-based implementation, we don’t need to declare the type of data explicitly. TypeScript automatically infers it from the DatabaseService interface.

Since you’ve explicitly stated that the function returns a DatabaseService, TypeScript knows that the createTicket method should match the interface's signature. Therefore, it automatically infers the parameter and return types without you needing to specify them explicitly.

“So, why does this work?”

The reasons for this difference

Historical and Design Choices

  • Classes in TypeScript were designed to be more explicit, following patterns from languages like Java or C#.
  • Functions and object literals were designed to be more flexible and leverage TypeScript’s powerful type inference capabilities.

Type Checking Approach

  • For classes, TypeScript checks if the class correctly implements the interface by comparing the explicitly declared methods.
  • For functions returning an interface type, TypeScript uses structural typing to check if the returned object matches the interface structure.

Contextual Typing

  • In the function version, the return type provides context for TypeScript to infer the method signatures.
  • Classes don’t have this surrounding context, so explicit annotations are required.

Flexibility vs Explicitness

  • The function approach offers more flexibility and can be less verbose.
  • The class approach is more explicit, which can benefit readability and catching errors early in larger codebases.

Conclusion

Both approaches have their merits, and the choice between them often comes down to coding style preferences and specific project requirements. The function approach can lead to more concise code, while the class approach provides more explicit type-checking at the cost of verbosity.

--

--

No responses yet