Skip to content
TkDodo's blog

Calling JavaScript from TypeScript

Sep 4, 2020 — TypeScript, JavaScript, JsDoc
Calling JavaScript from TypeScript
Photo by Antoine Barrès

There is nothing better than starting a new project, on a green field. You can choose all the latest tech you want, and you can begin with great types right from the start.

Obviously, you then wake up from your dream and realise you have to maintain a project with 150k lines of legacy JavaScript code. If you are lucky, the team started to gradually migrate the codebase to TypeScript.

But it will take some time to “get there”. Until then, you will need some interoperability between JavaScript and TypeScript.

Being in a JS file and calling a function defined in a .ts is trivial - it just works™. But what about the other way around? Turns out - that’s not so easy.

Example

Suppose you have a util function that you would like to import. It could be something as simple as:

utils.js
export const sum = ({ first, second, third }) =>
first + second + (third ?? 0)

A stupid example, I know, but it’ll do.

Setting up tsconfig.json

You’re gonna have to set allowJs: true in your tsconfig if you want to be able to import that file. Otherwise, your import will error with:

Of course, I am assuming here that you have noImplicitAny turned on as well 😊.

So with allowJs, TypeScript will start to accept .js files, and perform rudimentary type inference on them. The sum util will now be inferred as:

export const sum: function({ first: any, second: any, third: any }): any

Which is good enough, not type-safe at all, but that wasn’t part of the requirement. With that, we are all set-up. That wasn’t hard, so where’s the catch?

The catch

Maybe you’ve already noticed: The third parameter is actually optional. So we would like to call our function like that:

the-catch
sum({ first: 1, second: 2 })

Comparing this to the inferred type above, we will naturally get:

Solutions

There are multiple solutions to this problem, so you’ll have to decide for yourself which one is best suited for your specific case:

use .d.ts files

You can turn off allowJs and write declaration files (opens in a new window) for all your JavaScript files. Depending on the amount of files, this might be feasible, or not. It can be as easy as this any stub:

utils.d.ts
export const sum: any

This is substantially worse than the inferred version. You can of course be more specific than that, but you have to do it manually. And you have to remember to keep the both files in sync, so I’m not a big fan of this solution.

Don’t destruct

The described problem is actually due to typescript performing better inference if you use destructuring. We could change the implementation to:

no-destruct-utils
export const sum = (params) =>
params.first + params.second + (params.third ?? 0)

Now, TypeScript will just infer params as any, and we are again good to go. Especially if you are working with React components, destructing props is very common, so I’d also give this a pass.

Assign default parameters

default-params
export const sum = ({ first, second, third = 0 }) => first + second + third

I like this solution a lot, because the implementation is actually easier than before. The function’s interface now shows what is optional, which is why TypeScript also knows that. This works well for variables where the default is clear, like booleans, where you can easily default to false.

If you don’t know what a good default value would be, you could even cheat a bit and do this:

cheating-default-params
export const sum = ({ first, second, third = undefined }) =>
first + second + (third ?? 0)

🤯

undefined will also be the default value even if you don’t explicitly specify it, but now, TypeScript will let you. This is a non-invasive change, so if you have complex types where you can’t easily come up with a default value, this seems like a good alternative.

Convert the file to TypeScript

ts-all-the-things
type Params = {
first: number
second: number
third?: number
}
export const sum = ({ first, second, third }: Params): number =>
first + second + (third ?? 0)

The long-term thing you probably want to do anyways - convert it to TypeScript. If it’s feasible - go for this option.

Use JsDoc

This is the last option I have to offer, and I kinda like it because it represents the middle ground between things just being any and converting the whole file to TypeScript right away.

I never really understood why you would need this, but now I do. Adding JsDoc annotations to your JavaScript functions will:

with-js-docs
/**
* @param {{ first: number, second: number, third?: number }} params
* @returns {number}
*/
export const sum = ({ first, second, third }) =>
first + second + (third ?? 0)

Of course, you can also just type them to any or omit the return type. You can be as specific as you want.

Bonus: TypeChecking js files

If you add the // @ts-check comment at the top of your js file, it will be type-checked almost like all your typescript files, and JsDoc annotations will be honoured 😮. You can read more about the differences here (opens in a new window).

What I ended up doing

I used JsDoc (opens in a new window) for the first time today when I had this exact problem. I chose it over the other options because:


What would you do? Let me know in the comments below. ⬇️

Like the monospace font in the code blocks?

Check out monolisa.dev

Bytes - the JavaScript Newsletter that doesn't suck