Typescript tips for legacy projects: Type only you need

Typescript tips for legacy projects: Type only you need

When you introduce Typescript in a legacy project, or you are using a library that doesn’t provide types, you might be tempted to use any for the types you will need. But this is not a good idea, because you are losing all the benefits of Typescript. any it’s something you must remove from code, and from your mind.

Type a shape progressively

It’s very common to have an object with a lot of properties, and trying to type completely the shape (that is the final goal) of an object you don’t completely understand can be an overwhelming task.

But you can start creating the type from the consumer’s point of view. In this approach, you type the elements of the object you need

This strategy gives you a starting point to define a structure with simplicity and avoiding the any type.

Let’s show you an example:

Imagine your legacy app provides you with a function (that doesn’t have the typescript definition) that gets a list of users from the API, and you need to write a function to calculate the age of a user.


function getUser(id) {
  return {
    birthday: new Date('1980-01-01'),
    name: 'John',
    surname: 'Smith',
    role: 'user',
    accounts: ...
    ...
  }
}

The data structure the function returns is big and complex, and some users have different fields, for example you realize for the users with the role admin you have a field level for others who don’t.

You also realize all the users have the birthdate field. That is an important field for you. With these requirements, your function doesn’t need other data from the user, so you can start to type your user structure

export interface User {
  birthday: Date;
}

function userAge(user: User): number {
  const diffMs = Date.now() - user.birthday.getTime();
  const ageDt = new Date(diffMs);
  return Math.abs(ageDt.getUTCFullYear() - 1970);   
}

Ok, but now you want to type the return of the function that returns the user. But doing function getUser(id): User you will get a type error as the function returns more fields than the birthday field:

img.png

You need to let know typescript you only know the user has the field birthday and more field but you don’t know them. Writing that in typescript:

interface User {
  [x: string]: unknown
  birthday: Date
}

Well, it’s still a kind of any, but more restricted, for example const user2 = { name: 'Mike'} doesn’t fit the type user as the field birthday is missing.

Again, this is the starting point to type the user object without understanding the full object and with the minimum effort, this is much better than just any as when you know completely the object you don’t need to change the userAge function.

If you or a teammate add a new function or method that provides more knowledge of the object you can just continue completing the interface, for example if you discover a function in the legacy code to get the full name of the user, and the function lets you know (for example with the check it does) the name is always present, but not the surname, so you can complete the type with:

interface User {
  [x: string]: unknown
  name: string
  surname?: string
  birthday: Date
}

With time you will get a complete type for the user object, and you will be able to remove the [x: string]: unknown part.

interface User {
  [x: string]: unknown
  birthday: Date
}

function getUser(): User {
 return {
  name: 'John',
  surname: 'Smith',
  birthday: new Date(1999, 6, 12)
 }
}


function userAge<T>(user: User): number {
   const diffMs = Date.now() - user.birthday.getTime();
  const ageDt = new Date(diffMs);
  return Math.abs(ageDt.getUTCFullYear() - 1970);   
}

console.log(userAge(getUser()))

Run the code in the playground

Type the function, consts, etc you need

Nowadays, most of the js libraries include the type definition, the library can expose it or via an external package like DefinitelyTyped. But sometimes the types are not available, maybe because the library is old and/or because the library is not popular enough to have a type definition, or just because the library is private and only available in your company (a.k.a. legacy library).

The goal is to type (or declare in this case) completely the library, but you can just type the function, const, etc you will need. To let typescript know you are declaring your module or library you need to use the declare keyword and the module (library) name, for example:

declare module 'my-library' {
  export function theFunctionIUse(a: number, b: number): number;
  export const libraryConst: number;
  ...
}

This can be in any file of your project and only applies to it. if you want to share the declaration with other files you can create a package with the type definition (Check an example on DefinitelyTyped) and publish it in npm or in your company registry.

Summary

The goal is to type completely the legacy code and the libraries, but step by step. You can apply this strategy to improve the type “on demand” and avoid the any type, bearing in mind this is not the correct solution, it’s a path to the correct solution.