BattlefyBlogHistoryOpen menu
Close menuHistory

3 ways to type a closure–most of them bad

Ronald Chen August 29th 2022

JavaScript is complicated and TypeScript needs to be just as sophisticated to meet the challenge. This leads to seemingly many ways to do the same thing in TypeScript, but they all have slightly different reasons for existing. This makes it hard to tell what is the correct tool for the job.

Even for something as simple as typing a closure, there are 3 ways to do so.

The problem

Let's say we have a faulty closure and we would like TypeScript to catch our mistakes.

We want a closure that returns an object with a single numeric field answer. I.E. the type () => { answer: number }

Our faulty implementation will return an object, but be missing the answer field.

const closure = () => {
  // fail to actually return answer
  return {};
}

We want to make closure the type () => { answer: number }, so our first attempt is type assertions.

Type assertion

const closure = (() => {
  // fail to actually return answer
  return {};
}) as () => { answer: number };

We can check the type of closure and it is indeed what we wanted, but holup TypeScript didn't error. Why didn't TypeScript catch our mistake?

The type assertion const v = x as T tells TypeScript, "Make v of type T if typeof x can be a T."

The problem is our faulty implementation is of the type () => object. TypeScript only checks, "Can a () => object be a () => { answer: number }?" Since the answer is yes, there is no error.

Perhaps the more obvious way to type a closure is to use a type annotation.

Type annotation

const closure: () => { answer: number } = () => {
  //  ^^^^^^^ errors
  // fail to actually return answer
  return {};
};

// Type '() => {}' is not assignable to type '() => { answer: number; }'.
//   Property 'answer' is missing in type '{}' but required in type '{ answer: number; }'.

Great success! TypeScript has caught our mistake, but it is pointing at a type failure with closure instead of pointing where the mistake was made.

This is because const v: T = x means, "v is type T, error if typeof x isn't a T. We have again told TypeScript exactly what to do and has obeyed perfectly.

How we do get TypeScript to point us to the actual mistake? We can use one final form, inferred types.

Inferred type

const closure = (): { answer: number } => {
  // fail to actually return answer
  return {};
  //^^^^^^^^ errors
};

// Property 'answer' is missing in type '{}' but required in type '{ answer: number; }'.

Finally, TypeScript is precisely pointing to where the mistake was made. But why is this called inferred type when we have a return type annotation? Inferred type here means type for closure is inferred by the implementation.

When we write, const v = x;, the type of v is inferred by typeof x.

By keeping the type annotation within the implementation, we get our desired outcome that closure is the type of () => { answer: number } and allows TypeScript to give us better error messages.

Playground with all the code samples and proof all closures are of the expected type.

Do you want to learn how to write TypeScript that errors well for your fellow humans? You're in luck, Battlefy is hiring.

2022

Powered by
BATTLEFY