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.
- Use tab-indentation and semicolons.
- The definition should target the latest TypeScript version.
- Exported properties/methods should be documented (see below).
- The definition should be tested (see below).
- When you have to use Node.js types, install the
@types/node
package as a dev dependency. Do not add a/// <reference types="node"/>
triple-slash reference to the top of the definition file. - Third-party library types (everything in the
@types/*
namespace) must be installed as direct dependencies, if required. Use imports, not triple-slash references. - Ensure you're not falling for any of the common mistakes.
- For packages with a default export, use
export default function foo(…)
syntax. - Do not use
namespace
. - Use the name
"types"
and not"typings"
for the TypeScript definition field in package.json. - Place
"types"
in package.json after all official package properties, but before custom properties, preferably after"dependencies"
and/or"devDependencies"
. - If the entry file in the package is named
index.js
, name the type definition fileindex.d.ts
and put it in root.
You don't need to add atypes
field to package.json as TypeScript will infer it from the name. - Add the type definition file to the
files
field in package.json. - The pull request should have the title
Add TypeScript definition
. (Copy-paste it so you don't get it wrong) - Help review other pull requests that adds a type definition.
Check out this, this, and this example for how it should be done.
- Types should not have namespaced names;
type Options {}
, nottype FooOptions {}
, unless there are multipleOptions
interfaces. - Use the array shorthand type notation;
number[]
, notArray<number>
. - Use the
readonly number[]
notation; notReadonlyArray<number>
. - Prefer using the
unknown
type instead ofany
whenever possible. - Don't use abbreviation for type/variable/function names;
function foo(options: Options)
, notfunction foo(opts: Opts)
. - When there are more than one generic type variable in a method, they should have descriptive names;
type Mapper<Element, NewElement> = …
, nottype Mapper<T, U> = …
. - Don't prefix the name of interfaces with
I
;Options
, notIOptions
. - Imports, destructuring, and object literals should not have spaces around the identifier;
{foo}
, not{ foo }
. - Don't use permissive types like
object
orFunction
. Use specific type-signatures likeRecord<string, number>
or(input: string) => boolean;
. - Use
Record<string, any>
for accepting objects with string index type andRecord<string, unknown>
for returning such objects. The reasonany
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:
- Don't prefix lines with
*
. - Don't hard-wrap.
- Put an empty line between type entries.
- Sentences should start with an uppercase character and end in a dot.
- There's an empty line after the function description.
- Parameters and the return value should be documented.
- There's a dash after the parameter name.
@param
should not include the parameter type.- If the parameter description just repeats the parameter name, leave it out.
- If the parameter is
options
it doesn't need a description. - If the function returns
void
or a wrappedvoid
likePromise<void>
, leave out@returns
. - If you include an
@example
, there should be a newline above it. The example itself should be wrapped with triple backticks (```
). - If the API accepts an options-object, define an
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 formattingDefault: Lorem Ipsum.
. - Use
@returns
, not@return
. - Ambient declarations can't have default parameters, so in the case of a default method parameter, document it in the parameter docs instead, as seen in the above example.
@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.
- Include as many code examples as possible. Borrow from the readme.
- Code examples should be fully functional and should include the import statement.
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:
- The test file should be named
index.test-d.ts
. tsd
supports top-levelawait
.- When testing promise-returning functions, don't use the
await
keyword. Instead, directly assert for aPromise
, like in the example above. When you useawait
, your function can potentially return a bare value without being wrapped in aPromise
, sinceawait
will happily accept non-Promise
values, rendering your test meaningless. - Use
const
assertions when you need to pass literal or readonly typed values to functions in your tests.