At my job I was recently working on a project where built a backend function that would take user data from the frontend and fill a PDF template with it. This PDF did not have editable fields so we had to manually create a map from field names to x-y coordinates on the template. Backend and frontend were both written in TypeScript and our Backend logic looked something like this:

class UserData {
  firstName: string;
  lastName: string;
  email: string;
}

const coordinates = {
  firstName: [20, 20],
  lastName: [140, 20],
  email: [20, 40],
  emailRepetition: [140, 40]
};

const expandedData = {
  ...userData,
  emailRepetition: userData.email
};

coordinates would hold the map from the key in UserData to the coordinates on the page. We also would sometimes expand the user data that was sent to match the form’s layout (e.g by duplicating the e-mail address above).

This worked perfectly fine until we noticed that we had added a typo to our coordinates object where the second line would look like this: firstname: [20, 20],. Note that the capitalization was no longer consitent with the defintion in UserData and therefore the data would not show up on our filled forms. I quite like TypeScript nowadays and had the feeling that there had to be a way to solve this. My idea was to somehow have TypeScript check that all of the keys in coordinates actually had to be keys in UserData as well.

Turns out, TypeScript has a little keyword called keyof which allow us to define just this. With it, we were able to change the code to now look like this:

class UserData {
  firstName: string;
  lastName: string;
  email: string;
}

type PDFAdditionalKeys = "emailRepetition" | "nationality";
type PDFUserDataKeys = keyof UserData;

type PDFKeys = PDFUserDataKeys | PDFAdditionalKeys;

type PDFCoordinates = { [key in PDFKeys]?: [number, number] };
type PDFPayload = { [key in PDFKeys]?: string };

const coordinates: PDFCoordinates = {
  firstName: [20, 20],
  lastName: [140, 20],
  email: [20, 40],
  emailRepetition: [140, 40]
};

const userData: UserData = {
  firstName: "Test",
  lastName: "Person",
  email: "test.person@example.org"
};

const expandedData: PDFPayload = {
  ...userData,
  emailRepetition: userData.email
};

Here, we are adding a couple more types: PDFAdditionalKeys is a union of additional keys that have to be filled in the PDF template but are not part of the UserData class. The PDFUserDataKeys uses the mentioned keyof keyword to get the keys out of UserData. In PDFKeys we create a simple union of the two types of allowed keys (coming from UserData or being additional fields). We can then define two new types: PDFCoordinates and PDFPayload. In both of them, we make sure that the keys that are used in objects of this type will only be the ones that we are expecting.

If we look at our previous example with the mixed up firstname, the compiler would now show the following output:

index.ts:16:3 - error TS2322: Type '{ firstname: number[]; lastName: [number, number]; email: [number, number]; emailRepetition: [num...' is not assignable to type 'PDFCoordinates'.
  Object literal may only specify known properties, but 'firstname' does not exist in type 'PDFCoordinates'. Did you mean to write 'firstName'?

Pretty specific and it even has a helpful suggestion. I’m growing to like TypeScript more and more.

What’s your favourite TypeScript feature that I should learn about? Or what other use cases do you see for keyof? Let me know on twitter.

PS: I’ve created an interactive version for you to play around with this. It uses an SVG as a source, not a PDF but the logic remains the same. Try it here