From 7f739d3fe7dd24886342bd546ecc1b994360c3bf Mon Sep 17 00:00:00 2001 From: James <5511220+Zamiell@users.noreply.github.com> Date: Wed, 13 Dec 2023 05:11:34 -0500 Subject: [PATCH] fix: typescript types (#1783) * fix: typescript types * fix: tests * fix: comment * fix: tests * fix: tests * docs: typescript info * docs: add more docs * docs: update typescript.md * chore: update tsd dep * chore: remove yarn * fix: tests * fix: readme --- README.md | 36 ++++--- docs/typescript.md | 74 ++++++++++++++ docsify/sidebar.md | 1 + pino.d.ts | 58 ++++++++++- test/types/pino-arguments.test-d.ts | 145 ++++++++++++++++++++++++++++ test/types/pino.test-d.ts | 10 +- 6 files changed, 302 insertions(+), 22 deletions(-) create mode 100644 docs/typescript.md create mode 100644 test/types/pino-arguments.test-d.ts diff --git a/README.md b/README.md index 25929756e..05be6a93d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ ![banner](pino-banner.png) # pino + [![npm version](https://img.shields.io/npm/v/pino)](https://www.npmjs.com/package/pino) [![Build Status](https://img.shields.io/github/actions/workflow/status/pinojs/pino/ci.yml)](https://github.com/pinojs/pino/actions) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) @@ -9,18 +10,28 @@ ## Documentation -* [Benchmarks ⇗](/docs/benchmarks.md) -* [API ⇗](/docs/api.md) -* [Browser API ⇗](/docs/browser.md) -* [Redaction ⇗](/docs/redaction.md) -* [Child Loggers ⇗](/docs/child-loggers.md) -* [Transports ⇗](/docs/transports.md) -* [Web Frameworks ⇗](/docs/web.md) -* [Pretty Printing ⇗](/docs/pretty.md) -* [Asynchronous Logging ⇗](/docs/asynchronous.md) -* [Ecosystem ⇗](/docs/ecosystem.md) -* [Help ⇗](/docs/help.md) -* [Long Term Support Policy ⇗](/docs/lts.md) +* [Readme](/) +* [API](/docs/api.md) +* [Browser API](/docs/browser.md) +* [Redaction](/docs/redaction.md) +* [Child Loggers](/docs/child-loggers.md) +* [Transports](/docs/transports.md) +* [Web Frameworks](/docs/web.md) +* [Pretty Printing](/docs/pretty.md) +* [Asynchronous Logging](/docs/asynchronous.md) +* [Usage With TypeScript](/docs/typescript.md) +* [Ecosystem](/docs/ecosystem.md) +* [Benchmarks](/docs/benchmarks.md) +* [Long Term Support](/docs/lts.md) +* [Help](/docs/help.md) + * [Log rotation](/docs/help.md#rotate) + * [Reopening log files](/docs/help.md#reopening) + * [Saving to multiple files](/docs/help.md#multiple) + * [Log filtering](/docs/help.md#filter-logs) + * [Transports and systemd](/docs/help.md#transport-systemd) + * [Duplicate keys](/docs/help.md#dupe-keys) + * [Log levels as labels instead of numbers](/docs/help.md#level-string) + * [Pino with `debug`](/docs/help.md#debug) ## Install @@ -64,7 +75,6 @@ For using Pino with a web framework see: * [Pino with Node core `http`](docs/web.md#http) * [Pino with Nest](docs/web.md#nest) - ## Essentials diff --git a/docs/typescript.md b/docs/typescript.md new file mode 100644 index 000000000..645b88a62 --- /dev/null +++ b/docs/typescript.md @@ -0,0 +1,74 @@ +# Usage With TypeScript + +## Introduction + +If you are using TypeScript, Pino should work out of the box without any additional configuration. This is because even though Pino is written in JavaScript, it includes [a TypeScript definitions file](https://github.com/pinojs/pino/blob/master/pino.d.ts) as part of its bundle. + +In new TypeScript projects, you will want to use the ESM import style, like this: + +```ts +import pino from "pino"; + +const logger = pino(); + +logger.info('hello world'); +``` + +Some edge-cases are listed below. + +## String Interpolation + +The TypeScript definitions are configured to detect string interpolation arguments like this: + +```ts +const foo: string = getFoo(); +logger.info("foo: %s", foo); +``` + +In this case, `%s` refers to a string, as explained in the [documentation for logging method parameters](https://getpino.io/#/docs/api?id=logger). + +If you use a string interpolation placeholder without a corresponding argument or with an argument of the wrong type, the TypeScript compiler will throw an error. For example: + +```ts +const foo: string = getFoo(); +logger.info("foo: %s"); // Error: Missing an expected argument. +logger.info("foo: %d", foo); // Error: `foo` is not a number. +``` + +## Validating the Object + +Pino supports [logging both strings and objects](https://getpino.io/#/docs/api?id=logger). If you are passing an object to a Pino logger, you might want to validate that the object is in the correct shape. You can do this with the [`satisfies` operator](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html) in the same way that you would in other kinds of TypeScript code. For example: + +```ts +const myObject = { + foo: "someString", + bar: "someString", +} satisfies MyObject; +logger.info(strictShape); +``` + +Note that passing the object type as the first generic parameter to the logger is no longer supported. + +## Higher Order Functions + +Unfortunately, the type definitions for the Pino logger may not work properly when invoking them from a higher order function. For example: + +```ts +setTimeout(logger, 1000, "A second has passed!"); +``` + +This is a valid invocation of the logger (i.e. simply passing a single string argument), but TypeScript will throw a spurious error. To work around this, one solution is to wrap the function invocation like this: + +```ts +setTimeout(() => { + logger("A second has passed!"); +}, 1000); +``` + +Another solution would be to perform a manual type assertion like this: + +```ts +setTimeout(logger as (message: string) => void, 1000, "A second has passed!"); +``` + +Obviously, using type assertions makes your code less safe, so use the second solution with care. diff --git a/docsify/sidebar.md b/docsify/sidebar.md index 75d4ef0c4..f5cb45c73 100644 --- a/docsify/sidebar.md +++ b/docsify/sidebar.md @@ -7,6 +7,7 @@ * [Web Frameworks](/docs/web.md) * [Pretty Printing](/docs/pretty.md) * [Asynchronous Logging](/docs/asynchronous.md) +* [Usage With TypeScript](/docs/typescript.md) * [Ecosystem](/docs/ecosystem.md) * [Benchmarks](/docs/benchmarks.md) * [Long Term Support](/docs/lts.md) diff --git a/pino.d.ts b/pino.d.ts index fb6337783..2d88d3f24 100644 --- a/pino.d.ts +++ b/pino.d.ts @@ -13,6 +13,7 @@ // Michel Nemnom // Igor Savin // James Bromwell +// Zamiell // TypeScript Version: 4.4 import type { EventEmitter } from "events"; @@ -127,6 +128,26 @@ export interface LoggerExtras extends Event flush(cb?: (err?: Error) => void): void; } +/** + * The valid string interpolation placeholders are documented here: + * https://getpino.io/#/docs/api?id=logger + */ +interface StringInterpolationLetterToType { + s: string; + d: number; + o: object; + O: object; + j: object; +} + +/** Helper type to extract the string interpolation placeholders and convert them to types. */ +type ExtractArgs = T extends `${string}%${infer R}` + ? R extends `${infer A}${infer B}` + ? A extends keyof StringInterpolationLetterToType + ? [StringInterpolationLetterToType[A], ...ExtractArgs] + : ExtractArgs + : ExtractArgs + : [] declare namespace pino { //// Exported types and interfaces @@ -313,11 +334,38 @@ declare namespace pino { } interface LogFn { - // TODO: why is this different from `obj: object` or `obj: any`? - /* tslint:disable:no-unnecessary-generics */ - (obj: T, msg?: string, ...args: any[]): void; - (obj: unknown, msg?: string, ...args: any[]): void; - (msg: string, ...args: any[]): void; + // The first overload has: + // - An object as the first argument. (But functions are explicitly disallowed, which count as objects.) + // - An optional string as the second argument. + // - N optional arguments after that corresponding to the string interpolation placeholders. + // e.g. + // logFn({ foo: "foo" }); + // logFn({ foo: "foo" }, "bar"); + // logFn({ foo: "foo" }, "Message with an interpolation value: %s", "bar"); + // logFn({ foo: "foo" }, "Message with two interpolation values: %s %d", "bar", 123); + ( + // We want to disallow functions, which count as the "object" type. + obj: never extends T ? (T extends Function ? never : T) : T, + msg?: Msg, + ...stringInterpolationArgs: ExtractArgs + ): void; + + // The second overload has: + // - A string as the first argument. + // - N optional arguments after that corresponding to the string interpolation placeholders. + // e.g. + // logFn("foo"); + // logFn("Message with an interpolation value: %s", "foo"); + // logFn("Message with two interpolation values: %s %d", "foo", 123); + (msg: Msg, ...stringInterpolationArgs: ExtractArgs): void; + + // The third overload has: + // - A `number` or `boolean` as the first argument. (`symbol` is explicitly disallowed.) + // - No additional arguments should be allowed. + // e.g. + // logFn(123); + // logFn(true); + (arg: number | boolean): void; } interface LoggerOptions { diff --git a/test/types/pino-arguments.test-d.ts b/test/types/pino-arguments.test-d.ts new file mode 100644 index 000000000..d6e52e1f8 --- /dev/null +++ b/test/types/pino-arguments.test-d.ts @@ -0,0 +1,145 @@ +import pino from "../../pino"; + +// This file tests the "LogFn" interface, located in the "pino.d.ts" file. + +const logger = pino(); + +// ---------------- +// 1 Argument Tests +// ---------------- + +// Works. +logger.info("Testing a basic string log message."); +logger.info("Using an unsupported string interpolation pattern like %x should not cause an error."); +logger.info({ foo: "foo" }); +logger.info(123); +logger.info(true); + +// Fails because these types are not supported. +// @ts-expect-error +logger.info(() => {}); +// @ts-expect-error +logger.info(Symbol("foo")); + +// ------------------------------------------- +// 2 Argument Tests (with string as first arg) +// ------------------------------------------- + +// Works +logger.info("Message with an interpolation value: %s", "foo"); +logger.info("Message with an interpolation value: %d", 123); +logger.info("Message with an interpolation value: %o", {}); + +// Fails because there isn't supposed to be a second argument. +// @ts-expect-error +logger.info("Message with no interpolation value.", "foo"); + +// Fails because we forgot the second argument entirely. +// @ts-expect-error +logger.info("Message with an interpolation value: %s"); +// @ts-expect-error +logger.info("Message with an interpolation value: %d"); +// @ts-expect-error +logger.info("Message with an interpolation value: %o"); + +// Fails because we put the wrong type as the second argument. +// @ts-expect-error +logger.info("Message with an interpolation value: %s", 123); +// @ts-expect-error +logger.info("Message with an interpolation value: %d", "foo"); +// @ts-expect-error +logger.info("Message with an interpolation value: %o", "foo"); + +// ------------------------------------------- +// 2 Argument Tests (with object as first arg) +// ------------------------------------------- + +// Works +logger.info({ foo: "foo" }, "bar"); + +// Fails because the second argument must be a string. +// @ts-expect-error +logger.info({ foo: "foo" }, 123); + +// ------------------------------------------- +// 3 Argument Tests (with string as first arg) +// ------------------------------------------- + +// Works +logger.info("Message with two interpolation values: %s %s", "foo", "bar"); +logger.info("Message with two interpolation values: %d %d", 123, 456); +logger.info("Message with two interpolation values: %o %o", {}, {}); + +// Fails because we forgot the third argument entirely. +// @ts-expect-error +logger.info("Message with two interpolation values: %s %s", "foo"); +// @ts-expect-error +logger.info("Message with two interpolation values: %d %d", 123); +// @ts-expect-error +logger.info("Message with two interpolation values: %o %o", {}); + +// Works +logger.info("Message with two interpolation values of different types: %s %d", "foo", 123); +logger.info("Message with two interpolation values of different types: %d %o", 123, {}); + +// Fails because we put the wrong type as the third argument. +// @ts-expect-error +logger.info("Message with two interpolation values of different types: %s %d", "foo", "bar"); +// @ts-expect-error +logger.info("Message with two interpolation values of different types: %d %o", 123, 456); + +// ------------------------------------------- +// 3 Argument Tests (with object as first arg) +// ------------------------------------------- + +// Works +logger.info({ foo: "foo" }, "Message with an interpolation value: %s", "foo"); +logger.info({ foo: "foo" }, "Message with an interpolation value: %d", 123); +logger.info({ foo: "foo" }, "Message with an interpolation value: %o", {}); + +// Fails because there isn't supposed to be a third argument. +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with no interpolation value.", "foo"); + +// Fails because we forgot the third argument entirely. +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with an interpolation value: %s"); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with an interpolation value: %d"); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with an interpolation value: %o"); + +// Fails because we put the wrong type as the third argument. +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with an interpolation value: %s", 123); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with an interpolation value: %d", "foo"); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with an interpolation value: %o", "foo"); + +// ------------------------------------------- +// 4 Argument Tests (with object as first arg) +// ------------------------------------------- + +// Works +logger.info({ foo: "foo" }, "Message with two interpolation values: %s %s", "foo", "bar"); +logger.info({ foo: "foo" }, "Message with two interpolation values: %d %d", 123, 456); +logger.info({ foo: "foo" }, "Message with two interpolation values: %o %o", {}, {}); + +// Fails because we forgot the third argument entirely. +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with two interpolation values: %s %s", "foo"); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with two interpolation values: %d %d", 123); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with two interpolation values: %o %o", {}); + +// Works +logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %s %d", "foo", 123); +logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %d %o", 123, {}); + +// Fails because we put the wrong type as the fourth argument. +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %s %d", "foo", "bar"); +// @ts-expect-error +logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %d %o", 123, 456); diff --git a/test/types/pino.test-d.ts b/test/types/pino.test-d.ts index eb99dce46..3e9e63ba0 100644 --- a/test/types/pino.test-d.ts +++ b/test/types/pino.test-d.ts @@ -14,7 +14,9 @@ info("the answer is %d", 42); info({ obj: 42 }, "hello world"); info({ obj: 42, b: 2 }, "hello world"); info({ obj: { aa: "bbb" } }, "another"); -setImmediate(info, "after setImmediate"); +// The type definitions will not work properly when using higher order functions, so we have to +// perform a manual type assertion. +setImmediate(info as (msg: string) => void, "after setImmediate"); error(new Error("an error")); const writeSym = pino.symbols.writeSym; @@ -254,7 +256,7 @@ pino({ name: "my-logger" }, destinationViaOptionsObject); try { throw new Error('Some error') -} catch (err) { +} catch (err: any) { log.error(err) } @@ -263,9 +265,9 @@ interface StrictShape { err?: unknown; } -info({ +info({ activity: "Required property", -}); +} satisfies StrictShape); const logLine: pino.LogDescriptor = { level: 20,