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/nodepackage 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.tsand put it in root.
You don't need to add atypesfield to package.json as TypeScript will infer it from the name. - Add the type definition file to the
filesfield 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 multipleOptionsinterfaces. - Use the array shorthand type notation;
number[], notArray<number>. - Use the
readonly number[]notation; notReadonlyArray<number>. - Prefer using the
unknowntype instead ofanywhenever 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
objectorFunction. 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 reasonanyis 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.
@paramshould not include the parameter type.- If the parameter description just repeats the parameter name, leave it out.
- If the parameter is
optionsit doesn't need a description. - If the function returns
voidor a wrappedvoidlikePromise<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
Optionstype as seen above. Document default option values using the@defaulttag (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.
@returnsshould 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. tsdsupports top-levelawait.- When testing promise-returning functions, don't use the
awaitkeyword. 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, sinceawaitwill happily accept non-Promisevalues, rendering your test meaningless. - Use
constassertions when you need to pass literal or readonly typed values to functions in your tests.