Style guide for adding type definitions to my npm packages
CC-BY-4.0 License
Style guide for adding type definitions to my npm packages
Open an issue if anything is unclear or if you have ideas for other checklist items.
This style guide assumes your package is native ESM.
@types/node
package as a dev dependency. Do not add a /// <reference types="node"/>
triple-slash reference to the top of the definition file.@types/*
namespace) must be installed as direct dependencies, if required. Use imports, not triple-slash references.export default function foo(…)
syntax.namespace
."types"
and not "typings"
for the TypeScript definition field in package.json."types"
in package.json after all official package properties, but before custom properties, preferably after "dependencies"
and/or "devDependencies"
.index.js
, name the type definition file index.d.ts
and put it in root.types
field to package.json as TypeScript will infer it from the name.files
field in package.json.Add TypeScript definition
. (Copy-paste it so you don't get it wrong)
Check out this, this, and this example for how it should be done.
type Options {}
, not type FooOptions {}
, unless there are multiple Options
interfaces.number[]
, not Array<number>
.readonly number[]
notation; not ReadonlyArray<number>
.unknown
type instead of any
whenever possible.function foo(options: Options)
, not function foo(opts: Opts)
.type Mapper<Element, NewElement> = …
, not type Mapper<T, U> = …
.I
; Options
, not IOptions
.{foo}
, not { foo }
.object
or Function
. Use specific type-signatures like Record<string, number>
or (input: string) => boolean;
.Record<string, any>
for accepting objects with string index type and Record<string, unknown>
for returning such objects. The reason any
is used for assignment is that TypeScript has special behavior for it:
The index signature
Record<string, any>
in TypeScript behaves specially: it’s a valid assignment target for any object type. This is a special rule, since types with index signatures don’t normally produce this behavior.
Make something read-only when it's not meant to be modified. This is usually the case for return values and option interfaces. Get familiar with the readonly
keyword for properties and array/tuple types. There's also a Readonly
type to mark all properties as readonly
.
Before:
type Point = {
x: number;
y: number;
children: Point[];
};
After:
type Point = {
readonly x: number;
readonly y: number;
readonly children: readonly Point[];
};
Don't use implicit global types except for built-ins or when they can't be imported.
Before:
export function getWindow(): Electron.BrowserWindow;
After:
import {BrowserWindow} from 'electron';
export function getWindow(): BrowserWindow;
Use a readable name when using named imports.
Before:
import {Writable} from 'node:stream';
After:
import {Writable as WritableStream} from 'node:stream';
Exported definitions should be documented with TSDoc. You can borrow text from the readme.
Example:
export type Options = {
/**
Allow negative numbers.
@default true
*/
readonly allowNegative?: boolean;
/**
Has the ultimate foo.
Note: Only use this for good.
@default false
*/
readonly hasFoo?: boolean;
/**
Where to save.
Default: [User's downloads directory](https://example.com)
@example
```
import add from 'add';
add(1, 2, {saveDirectory: '/my/awesome/dir'})
```
*/
readonly saveDirectory?: string;
};
/**
Add two numbers together.
@param x - The first number to add.
@param y - The second number to add.
@returns The sum of `x` and `y`.
*/
export default function add(x: number, y: number, options?: Options): number;
Note:
*
.@param
should not include the parameter type.options
it doesn't need a description.void
or a wrapped void
like Promise<void>
, leave out @returns
.@example
, there should be a newline above it. The example itself should be wrapped with triple backticks (```
).Options
type as seen above. Document default option values using the @default
tag (since type cannot have default values). If the default needs to be a description instead of a basic value, use the formatting Default: Lorem Ipsum.
.@returns
, not @return
.@returns
should not duplicate the type information unless it's impossible to describe it without.
@returns A boolean of whether it was enabled.
→ @returns Whether it was enabled.
The type definition should be tested with tsd
. Example of how to integrate it.
Example:
import {expectType} from 'tsd';
import delay from './index.js';
expectType<Promise<void>>(delay(200));
expectType<Promise<string>>(delay(200, {value: '🦄'}));
expectType<Promise<number>>(delay(200, {value: 0}));
expectType<Promise<never>>(delay.reject(200, {value: '🦄'}));
expectType<Promise<never>>(delay.reject(200, {value: 0}));
When it makes sense, also add a negative test using expectError()
.
Note:
index.test-d.ts
.tsd
supports top-level await
.await
keyword. Instead, directly assert for a Promise
, like in the example above. When you use await
, your function can potentially return a bare value without being wrapped in a Promise
, since await
will happily accept non-Promise
values, rendering your test meaningless.const
assertions when you need to pass literal or readonly typed values to functions in your tests.