Skip to content

Commit

Permalink
fix: typescript types (#1783)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
Zamiell authored Dec 13, 2023
1 parent 006df81 commit 7f739d3
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 22 deletions.
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
@@ -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/)
Expand All @@ -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

Expand Down Expand Up @@ -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)


<a name="essentials"></a>
## Essentials

Expand Down
74 changes: 74 additions & 0 deletions docs/typescript.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docsify/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
58 changes: 53 additions & 5 deletions pino.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
// Michel Nemnom <https://github.com/Pegase745>
// Igor Savin <https://github.com/kibertoad>
// James Bromwell <https://github.com/thw0rted>
// Zamiell <https://github.com/Zamiell>
// TypeScript Version: 4.4

import type { EventEmitter } from "events";
Expand Down Expand Up @@ -127,6 +128,26 @@ export interface LoggerExtras<CustomLevels extends string = never> 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> = T extends `${string}%${infer R}`
? R extends `${infer A}${infer B}`
? A extends keyof StringInterpolationLetterToType
? [StringInterpolationLetterToType[A], ...ExtractArgs<B>]
: ExtractArgs<B>
: ExtractArgs<R>
: []

declare namespace pino {
//// Exported types and interfaces
Expand Down Expand Up @@ -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 */
<T extends object>(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);
<T extends object, Msg extends string>(
// 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<Msg>
): 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 extends string>(msg: Msg, ...stringInterpolationArgs: ExtractArgs<Msg>): 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<CustomLevels extends string = never> {
Expand Down
145 changes: 145 additions & 0 deletions test/types/pino-arguments.test-d.ts
Original file line number Diff line number Diff line change
@@ -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);
10 changes: 6 additions & 4 deletions test/types/pino.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -254,7 +256,7 @@ pino({ name: "my-logger" }, destinationViaOptionsObject);

try {
throw new Error('Some error')
} catch (err) {
} catch (err: any) {
log.error(err)
}

Expand All @@ -263,9 +265,9 @@ interface StrictShape {
err?: unknown;
}

info<StrictShape>({
info({
activity: "Required property",
});
} satisfies StrictShape);

const logLine: pino.LogDescriptor = {
level: 20,
Expand Down

0 comments on commit 7f739d3

Please sign in to comment.