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
- No
import
statements outside of thedeclare
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
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 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)