Skip to content

Commit 7f739d3

Browse files
authored
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
1 parent 006df81 commit 7f739d3

File tree

6 files changed

+302
-22
lines changed

6 files changed

+302
-22
lines changed

README.md

+23-13
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
![banner](pino-banner.png)
22

33
# pino
4+
45
[![npm version](https://img.shields.io/npm/v/pino)](https://www.npmjs.com/package/pino)
56
[![Build Status](https://img.shields.io/github/actions/workflow/status/pinojs/pino/ci.yml)](https://github.com/pinojs/pino/actions)
67
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
@@ -9,18 +10,28 @@
910

1011
## Documentation
1112

12-
* [Benchmarks ⇗](/docs/benchmarks.md)
13-
* [API ⇗](/docs/api.md)
14-
* [Browser API ⇗](/docs/browser.md)
15-
* [Redaction ⇗](/docs/redaction.md)
16-
* [Child Loggers ⇗](/docs/child-loggers.md)
17-
* [Transports ⇗](/docs/transports.md)
18-
* [Web Frameworks ⇗](/docs/web.md)
19-
* [Pretty Printing ⇗](/docs/pretty.md)
20-
* [Asynchronous Logging ⇗](/docs/asynchronous.md)
21-
* [Ecosystem ⇗](/docs/ecosystem.md)
22-
* [Help ⇗](/docs/help.md)
23-
* [Long Term Support Policy ⇗](/docs/lts.md)
13+
* [Readme](/)
14+
* [API](/docs/api.md)
15+
* [Browser API](/docs/browser.md)
16+
* [Redaction](/docs/redaction.md)
17+
* [Child Loggers](/docs/child-loggers.md)
18+
* [Transports](/docs/transports.md)
19+
* [Web Frameworks](/docs/web.md)
20+
* [Pretty Printing](/docs/pretty.md)
21+
* [Asynchronous Logging](/docs/asynchronous.md)
22+
* [Usage With TypeScript](/docs/typescript.md)
23+
* [Ecosystem](/docs/ecosystem.md)
24+
* [Benchmarks](/docs/benchmarks.md)
25+
* [Long Term Support](/docs/lts.md)
26+
* [Help](/docs/help.md)
27+
* [Log rotation](/docs/help.md#rotate)
28+
* [Reopening log files](/docs/help.md#reopening)
29+
* [Saving to multiple files](/docs/help.md#multiple)
30+
* [Log filtering](/docs/help.md#filter-logs)
31+
* [Transports and systemd](/docs/help.md#transport-systemd)
32+
* [Duplicate keys](/docs/help.md#dupe-keys)
33+
* [Log levels as labels instead of numbers](/docs/help.md#level-string)
34+
* [Pino with `debug`](/docs/help.md#debug)
2435

2536
## Install
2637

@@ -64,7 +75,6 @@ For using Pino with a web framework see:
6475
* [Pino with Node core `http`](docs/web.md#http)
6576
* [Pino with Nest](docs/web.md#nest)
6677

67-
6878
<a name="essentials"></a>
6979
## Essentials
7080

docs/typescript.md

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# Usage With TypeScript
2+
3+
## Introduction
4+
5+
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.
6+
7+
In new TypeScript projects, you will want to use the ESM import style, like this:
8+
9+
```ts
10+
import pino from "pino";
11+
12+
const logger = pino();
13+
14+
logger.info('hello world');
15+
```
16+
17+
Some edge-cases are listed below.
18+
19+
## String Interpolation
20+
21+
The TypeScript definitions are configured to detect string interpolation arguments like this:
22+
23+
```ts
24+
const foo: string = getFoo();
25+
logger.info("foo: %s", foo);
26+
```
27+
28+
In this case, `%s` refers to a string, as explained in the [documentation for logging method parameters](https://getpino.io/#/docs/api?id=logger).
29+
30+
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:
31+
32+
```ts
33+
const foo: string = getFoo();
34+
logger.info("foo: %s"); // Error: Missing an expected argument.
35+
logger.info("foo: %d", foo); // Error: `foo` is not a number.
36+
```
37+
38+
## Validating the Object
39+
40+
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:
41+
42+
```ts
43+
const myObject = {
44+
foo: "someString",
45+
bar: "someString",
46+
} satisfies MyObject;
47+
logger.info(strictShape);
48+
```
49+
50+
Note that passing the object type as the first generic parameter to the logger is no longer supported.
51+
52+
## Higher Order Functions
53+
54+
Unfortunately, the type definitions for the Pino logger may not work properly when invoking them from a higher order function. For example:
55+
56+
```ts
57+
setTimeout(logger, 1000, "A second has passed!");
58+
```
59+
60+
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:
61+
62+
```ts
63+
setTimeout(() => {
64+
logger("A second has passed!");
65+
}, 1000);
66+
```
67+
68+
Another solution would be to perform a manual type assertion like this:
69+
70+
```ts
71+
setTimeout(logger as (message: string) => void, 1000, "A second has passed!");
72+
```
73+
74+
Obviously, using type assertions makes your code less safe, so use the second solution with care.

docsify/sidebar.md

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* [Web Frameworks](/docs/web.md)
88
* [Pretty Printing](/docs/pretty.md)
99
* [Asynchronous Logging](/docs/asynchronous.md)
10+
* [Usage With TypeScript](/docs/typescript.md)
1011
* [Ecosystem](/docs/ecosystem.md)
1112
* [Benchmarks](/docs/benchmarks.md)
1213
* [Long Term Support](/docs/lts.md)

pino.d.ts

+53-5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// Michel Nemnom <https://github.com/Pegase745>
1414
// Igor Savin <https://github.com/kibertoad>
1515
// James Bromwell <https://github.com/thw0rted>
16+
// Zamiell <https://github.com/Zamiell>
1617
// TypeScript Version: 4.4
1718

1819
import type { EventEmitter } from "events";
@@ -127,6 +128,26 @@ export interface LoggerExtras<CustomLevels extends string = never> extends Event
127128
flush(cb?: (err?: Error) => void): void;
128129
}
129130

131+
/**
132+
* The valid string interpolation placeholders are documented here:
133+
* https://getpino.io/#/docs/api?id=logger
134+
*/
135+
interface StringInterpolationLetterToType {
136+
s: string;
137+
d: number;
138+
o: object;
139+
O: object;
140+
j: object;
141+
}
142+
143+
/** Helper type to extract the string interpolation placeholders and convert them to types. */
144+
type ExtractArgs<T extends string> = T extends `${string}%${infer R}`
145+
? R extends `${infer A}${infer B}`
146+
? A extends keyof StringInterpolationLetterToType
147+
? [StringInterpolationLetterToType[A], ...ExtractArgs<B>]
148+
: ExtractArgs<B>
149+
: ExtractArgs<R>
150+
: []
130151

131152
declare namespace pino {
132153
//// Exported types and interfaces
@@ -313,11 +334,38 @@ declare namespace pino {
313334
}
314335

315336
interface LogFn {
316-
// TODO: why is this different from `obj: object` or `obj: any`?
317-
/* tslint:disable:no-unnecessary-generics */
318-
<T extends object>(obj: T, msg?: string, ...args: any[]): void;
319-
(obj: unknown, msg?: string, ...args: any[]): void;
320-
(msg: string, ...args: any[]): void;
337+
// The first overload has:
338+
// - An object as the first argument. (But functions are explicitly disallowed, which count as objects.)
339+
// - An optional string as the second argument.
340+
// - N optional arguments after that corresponding to the string interpolation placeholders.
341+
// e.g.
342+
// logFn({ foo: "foo" });
343+
// logFn({ foo: "foo" }, "bar");
344+
// logFn({ foo: "foo" }, "Message with an interpolation value: %s", "bar");
345+
// logFn({ foo: "foo" }, "Message with two interpolation values: %s %d", "bar", 123);
346+
<T extends object, Msg extends string>(
347+
// We want to disallow functions, which count as the "object" type.
348+
obj: never extends T ? (T extends Function ? never : T) : T,
349+
msg?: Msg,
350+
...stringInterpolationArgs: ExtractArgs<Msg>
351+
): void;
352+
353+
// The second overload has:
354+
// - A string as the first argument.
355+
// - N optional arguments after that corresponding to the string interpolation placeholders.
356+
// e.g.
357+
// logFn("foo");
358+
// logFn("Message with an interpolation value: %s", "foo");
359+
// logFn("Message with two interpolation values: %s %d", "foo", 123);
360+
<Msg extends string>(msg: Msg, ...stringInterpolationArgs: ExtractArgs<Msg>): void;
361+
362+
// The third overload has:
363+
// - A `number` or `boolean` as the first argument. (`symbol` is explicitly disallowed.)
364+
// - No additional arguments should be allowed.
365+
// e.g.
366+
// logFn(123);
367+
// logFn(true);
368+
(arg: number | boolean): void;
321369
}
322370

323371
interface LoggerOptions<CustomLevels extends string = never> {

test/types/pino-arguments.test-d.ts

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import pino from "../../pino";
2+
3+
// This file tests the "LogFn" interface, located in the "pino.d.ts" file.
4+
5+
const logger = pino();
6+
7+
// ----------------
8+
// 1 Argument Tests
9+
// ----------------
10+
11+
// Works.
12+
logger.info("Testing a basic string log message.");
13+
logger.info("Using an unsupported string interpolation pattern like %x should not cause an error.");
14+
logger.info({ foo: "foo" });
15+
logger.info(123);
16+
logger.info(true);
17+
18+
// Fails because these types are not supported.
19+
// @ts-expect-error
20+
logger.info(() => {});
21+
// @ts-expect-error
22+
logger.info(Symbol("foo"));
23+
24+
// -------------------------------------------
25+
// 2 Argument Tests (with string as first arg)
26+
// -------------------------------------------
27+
28+
// Works
29+
logger.info("Message with an interpolation value: %s", "foo");
30+
logger.info("Message with an interpolation value: %d", 123);
31+
logger.info("Message with an interpolation value: %o", {});
32+
33+
// Fails because there isn't supposed to be a second argument.
34+
// @ts-expect-error
35+
logger.info("Message with no interpolation value.", "foo");
36+
37+
// Fails because we forgot the second argument entirely.
38+
// @ts-expect-error
39+
logger.info("Message with an interpolation value: %s");
40+
// @ts-expect-error
41+
logger.info("Message with an interpolation value: %d");
42+
// @ts-expect-error
43+
logger.info("Message with an interpolation value: %o");
44+
45+
// Fails because we put the wrong type as the second argument.
46+
// @ts-expect-error
47+
logger.info("Message with an interpolation value: %s", 123);
48+
// @ts-expect-error
49+
logger.info("Message with an interpolation value: %d", "foo");
50+
// @ts-expect-error
51+
logger.info("Message with an interpolation value: %o", "foo");
52+
53+
// -------------------------------------------
54+
// 2 Argument Tests (with object as first arg)
55+
// -------------------------------------------
56+
57+
// Works
58+
logger.info({ foo: "foo" }, "bar");
59+
60+
// Fails because the second argument must be a string.
61+
// @ts-expect-error
62+
logger.info({ foo: "foo" }, 123);
63+
64+
// -------------------------------------------
65+
// 3 Argument Tests (with string as first arg)
66+
// -------------------------------------------
67+
68+
// Works
69+
logger.info("Message with two interpolation values: %s %s", "foo", "bar");
70+
logger.info("Message with two interpolation values: %d %d", 123, 456);
71+
logger.info("Message with two interpolation values: %o %o", {}, {});
72+
73+
// Fails because we forgot the third argument entirely.
74+
// @ts-expect-error
75+
logger.info("Message with two interpolation values: %s %s", "foo");
76+
// @ts-expect-error
77+
logger.info("Message with two interpolation values: %d %d", 123);
78+
// @ts-expect-error
79+
logger.info("Message with two interpolation values: %o %o", {});
80+
81+
// Works
82+
logger.info("Message with two interpolation values of different types: %s %d", "foo", 123);
83+
logger.info("Message with two interpolation values of different types: %d %o", 123, {});
84+
85+
// Fails because we put the wrong type as the third argument.
86+
// @ts-expect-error
87+
logger.info("Message with two interpolation values of different types: %s %d", "foo", "bar");
88+
// @ts-expect-error
89+
logger.info("Message with two interpolation values of different types: %d %o", 123, 456);
90+
91+
// -------------------------------------------
92+
// 3 Argument Tests (with object as first arg)
93+
// -------------------------------------------
94+
95+
// Works
96+
logger.info({ foo: "foo" }, "Message with an interpolation value: %s", "foo");
97+
logger.info({ foo: "foo" }, "Message with an interpolation value: %d", 123);
98+
logger.info({ foo: "foo" }, "Message with an interpolation value: %o", {});
99+
100+
// Fails because there isn't supposed to be a third argument.
101+
// @ts-expect-error
102+
logger.info({ foo: "foo" }, "Message with no interpolation value.", "foo");
103+
104+
// Fails because we forgot the third argument entirely.
105+
// @ts-expect-error
106+
logger.info({ foo: "foo" }, "Message with an interpolation value: %s");
107+
// @ts-expect-error
108+
logger.info({ foo: "foo" }, "Message with an interpolation value: %d");
109+
// @ts-expect-error
110+
logger.info({ foo: "foo" }, "Message with an interpolation value: %o");
111+
112+
// Fails because we put the wrong type as the third argument.
113+
// @ts-expect-error
114+
logger.info({ foo: "foo" }, "Message with an interpolation value: %s", 123);
115+
// @ts-expect-error
116+
logger.info({ foo: "foo" }, "Message with an interpolation value: %d", "foo");
117+
// @ts-expect-error
118+
logger.info({ foo: "foo" }, "Message with an interpolation value: %o", "foo");
119+
120+
// -------------------------------------------
121+
// 4 Argument Tests (with object as first arg)
122+
// -------------------------------------------
123+
124+
// Works
125+
logger.info({ foo: "foo" }, "Message with two interpolation values: %s %s", "foo", "bar");
126+
logger.info({ foo: "foo" }, "Message with two interpolation values: %d %d", 123, 456);
127+
logger.info({ foo: "foo" }, "Message with two interpolation values: %o %o", {}, {});
128+
129+
// Fails because we forgot the third argument entirely.
130+
// @ts-expect-error
131+
logger.info({ foo: "foo" }, "Message with two interpolation values: %s %s", "foo");
132+
// @ts-expect-error
133+
logger.info({ foo: "foo" }, "Message with two interpolation values: %d %d", 123);
134+
// @ts-expect-error
135+
logger.info({ foo: "foo" }, "Message with two interpolation values: %o %o", {});
136+
137+
// Works
138+
logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %s %d", "foo", 123);
139+
logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %d %o", 123, {});
140+
141+
// Fails because we put the wrong type as the fourth argument.
142+
// @ts-expect-error
143+
logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %s %d", "foo", "bar");
144+
// @ts-expect-error
145+
logger.info({ foo: "foo" }, "Message with two interpolation values of different types: %d %o", 123, 456);

test/types/pino.test-d.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ info("the answer is %d", 42);
1414
info({ obj: 42 }, "hello world");
1515
info({ obj: 42, b: 2 }, "hello world");
1616
info({ obj: { aa: "bbb" } }, "another");
17-
setImmediate(info, "after setImmediate");
17+
// The type definitions will not work properly when using higher order functions, so we have to
18+
// perform a manual type assertion.
19+
setImmediate(info as (msg: string) => void, "after setImmediate");
1820
error(new Error("an error"));
1921

2022
const writeSym = pino.symbols.writeSym;
@@ -254,7 +256,7 @@ pino({ name: "my-logger" }, destinationViaOptionsObject);
254256

255257
try {
256258
throw new Error('Some error')
257-
} catch (err) {
259+
} catch (err: any) {
258260
log.error(err)
259261
}
260262

@@ -263,9 +265,9 @@ interface StrictShape {
263265
err?: unknown;
264266
}
265267

266-
info<StrictShape>({
268+
info({
267269
activity: "Required property",
268-
});
270+
} satisfies StrictShape);
269271

270272
const logLine: pino.LogDescriptor = {
271273
level: 20,

0 commit comments

Comments
 (0)