Working with JS modules in TypeScript: Type Definition files

Tianyi Song
3 min readAug 25, 2020

--

Read this article with no pay wall at my blog: https://tsong.co/blog/working-with-js-modules-in-typescript-type-definition-files/

TypeScript is getting more and more popular, and more teams are adopting it to achieve better type safety than JavaScript code.

The design of TypeScript also makes the transition straightforward — it’s designed to work alongside JavaScript in the same code base. With type inference, the compiler is able to infer limited types from the JS code itself and JSDoc in JavaScript code. However, the type checker can’t do its magic in imported packages, so the imported types are often any . This obviously defeats the purpose of a strict type checking. Not cool.

Let’s see how to type check these modules in the consuming code.

Using DefinitelyTyped modules

Most of the well-known packages have community-contributed type definition for them, hosted in the DefinitelyTyped repo on GitHub, so you can just install the type definition files from NPM. For example, npm install @types/styled-components.

Writing Type Definitions

However, often in larger teams, there are some in-house JS packages, for example UI component modules. While it would be nice if they are written in TypeScript, we need a way to work with them in the consuming TypeScript code base.

Creating type definition .d.ts files (sometimes called ambient type declarations) is a good way to interoperate with these modules in TypeScript.

What It Looks Like

// located at types/internal-ui-components/inputs/index.d.tsdeclare module '@internal-ui-components/inputs' {
export type DropdownProps = {
className: string
dropdownListClassName?: string
dropdownOptionClassName?: string
disabled?: boolean
placeholder: string
options: string[]
onSelect(number): void
} & typeof import('@internal-ui-components/inputs').Dropdown.defaultProps;

export const Dropdown = React.forwardRef<HTMLDivElement, DropdownProps>(); // or use the ReturnType utility type:
}

Some Tips

  • No `import` statements outside of the `declare` block

The reason is that the files with top-level import statements won’t get
recognized as an ambient (global) type definition file, but rather a normal
module.

If there’s a need to import modules, use the inline import statement. See StackOverflow discussions here. The example above also used inline import on Line 11, as a workaround with React defaultProps.

  • If there’s a type (often returned by an existing function) that’s too complex to type out, we can just “call” that function, or use the ReturnType<typeof f> to get the return type

This shouldn’t have any runtime implication, since type checking is done at compile time, and the type definitions are erased at run time. Also, see more on the ReturnType here.

.d.ts File Location

Technically, the type definitions files can be located anywhere in the code base, as long as the file is visible to the TypeScript compiler. The tsc compiler will look for all .d.ts files based on the include and exclude rules, defined in tsconfig.json ‘s compilerOptions .

However, for the sake of clear organization, a path for a UI component .d.ts file could be:

types/internal-ui-components/inputs/index.d.ts

Note that, adding the @ identifier in the path name, i.e. using types/@internal-ui-components/causes some issue for the TypeScript compiler to locate the file.

Photo by Bruno Bergher on Unsplash

What’s Next

The next step is to convert the upstream JavaScript module (in this case, @internal-ui-components/inputs) to TypeScript, too.

In the upstream repo, you could configure the TypeScript compiler to emit type declaration files. Then, in the package’s package.json , point the types property to the generated index.d.ts . And, you’re done!

--

--