Calling JavaScript from TypeScript
Tags: javascript, typescript, webdev, programming
Published: 2021-01-13 20:48:35

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&trade;. 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:

```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:

```
TS7016: Could not find a declaration file for module './utils'.
'src/utils.js' implicitly has an 'any' type.
```

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:

```ts
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:

```ts
sum({ first: 1, second: 2 })
```

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

```
TS2345: Argument of type '{ first: number; second: number; }' is not assignable to parameter of type '{ first: any; second: any; third: any; }'.
    Property 'third' is missing in type '{ first: number; second: number; }' but required in type '{ first: any; second: any; third: any; }'.
```

# 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](https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) 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:

```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:

```js
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

```js
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:

```js
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
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:

-   Help TypeScript with type inference, thus making your call sides more safe.
-   Give you IntelliSense in your IDE.
-   Make it easier to finally migrate to TypeScript when the time is right.

```js
/**
 * @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](https://github.com/Microsoft/TypeScript/wiki/Type-Checking-JavaScript-Files).

# What I ended up doing

I used [JsDoc](https://www.typescriptlang.org/docs/handbook/jsdoc-supported-types.html) for the first time today when I had this exact problem.
I chose it over the other options because:

-   adding .d.ts files is tedious to maintain and will make my IDE stop navigating to the actual source 😒
-   I wanted to keep the destructuring 😕
-   Default parameters were hard to come up with as my case was much more complex 🧐
-   The file in question had 120+ lines of code 🤨
-   I wanted to make it easier for us to migrate when we fully convert that file 🚀

---

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