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:
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:
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:
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 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:
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:
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
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:
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
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.
/**
* @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.
What I ended up doing
I used JsDoc 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 ⬇️
Top comments (0)