Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: typescript types #1783

Merged
merged 12 commits into from
Dec 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Zamiell marked this conversation as resolved.
Show resolved Hide resolved

## 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
"tape": "^5.5.3",
"through2": "^4.0.0",
"ts-node": "^10.9.1",
"tsd": "^0.24.1",
"tsd": "^0.29.0",
"typescript": "^5.1.3",
"winston": "^3.7.2"
},
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<Options = LoggerOptions> extends EventEmitter {
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 {
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);
Loading
Loading