Tianyi Song

Working With JS Modules in Typescript: Type Definition

The design of TypeScript 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.ts
declare 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;
  

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

Some Tips

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.

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 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.

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!


Originally posted on Medium (paywalled)