From bef7ba14303997a05cdd1ea0d28ffefeee75c993 Mon Sep 17 00:00:00 2001 From: Bert Belder Date: Tue, 15 Jan 2019 20:30:43 +0100 Subject: [PATCH] Add API design guidelines (#119) --- README.md | 104 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/README.md b/README.md index 03bcae0fa..aab0f4522 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,110 @@ Example: Instead of `file-server.ts` use `file_server.ts`. More specifically, code should be wrapped at 80 columns and use 2-space indentation and use camel-case. Use `//format.ts` to invoke prettier. +### Exported functions: max 2 args, put the rest into an options object. + +When designing function interfaces, stick to the following rules. + +1. A function that is part of the public API takes 0-2 required arguments, + plus (if necessary) an options object (so max 3 total). + +2. Optional parameters should generally go into the options object. + + An optional parameter that's not in an options object might be acceptable + if there is only one, and it seems inconceivable that we would add more + optional parameters in the future. + +3. The 'options' argument is the only argument that is a regular 'Object'. + + Other arguments can be objects, but they must be distinguishable from a + 'plain' Object runtime, by having either: + + - a distinguishing prototype (e.g. `Array`, `Map`, `Date`, `class MyThing`) + - a well-known symbol property (e.g. an iterable with `Symbol.iterator`). + + This allows the API to evolve in a backwards compatible way, even when the + position of the options object changes. + +```ts +// BAD: optional parameters not part of options object. (#2) +export function resolve( + hostname: string, + family?: "ipv4" | "ipv6", + timeout?: number +): IPAddress[] {} + +// GOOD. +export interface ResolveOptions { + family?: "ipv4" | "ipv6"; + timeout?: number; +} +export function resolve( + hostname: string, + options: ResolveOptions = {} +): IPAddress[] {} +``` + +```ts +export interface Environment { + [key: string]: string; +} + +// BAD: `env` could be a regular Object and is therefore indistinguishable +// from an options object. (#3) +export function runShellWithEnv(cmdline: string, env: Environment): string {} + +// GOOD. +export interface RunShellOptions { + env: Environment; +} +export function runShellWithEnv( + cmdline: string, + options: RunShellOptions +): string {} +``` + +```ts +// BAD: more than 3 arguments (#1), multiple optional parameters (#2). +export function renameSync( + oldname: string, + newname: string, + replaceExisting?: boolean, + followLinks?: boolean +) {} + +// GOOD. +interface RenameOptions { + replaceExisting?: boolean; + followLinks?: boolean; +} +export function renameSync( + oldname: string, + newname: string, + options: RenameOptions = {} +) {} +``` + +```ts +// BAD: too many arguments. (#1) +export function pwrite( + fd: number, + buffer: TypedArray, + offset: number, + length: number, + position: number +) {} + +// BETTER. +export interface PWrite { + fd: number; + buffer: TypedArray; + offset: number; + length: number; + position: number; +} +export function pwrite(options: PWrite) {} +``` + ### Use JS Doc to document exported machinery We strive for complete documentation. Every exported symbol ideally should have