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,