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 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion 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 @@ -18,6 +19,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)
* [Help ⇗](/docs/help.md)
* [Long Term Support Policy ⇗](/docs/lts.md)
Expand Down Expand Up @@ -64,7 +66,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.
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);
16 changes: 14 additions & 2 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 @@ -108,7 +110,6 @@ pino({
});

pino({ base: null });
// @ts-expect-error
if ("pino" in log) console.log(`pino version: ${log.pino}`);

expectType<void>(log.flush());
Expand Down Expand Up @@ -264,9 +265,19 @@ interface StrictShape {
err?: unknown;
}

// The following generic parameter is no longer supported:

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

// Instead, the `satisfies` operator should be used like you would with any other TypeScript code, like in the below example.

info({
activity: "Required property",
} satisfies StrictShape);
Zamiell marked this conversation as resolved.
Show resolved Hide resolved

const logLine: pino.LogDescriptor = {
level: 20,
Expand Down Expand Up @@ -300,6 +311,7 @@ const customBaseLogger: CustomBaseLogger = {

// custom levels
const log3 = pino({ customLevels: { myLevel: 100 } })
// @ts-expect-error We intentionally cause a run-time error here.
expectError(log3.log())
log3.level = 'myLevel'
log3.myLevel('')
Expand Down