Skip to content
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
26 changes: 20 additions & 6 deletions pino.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -319,12 +319,27 @@ declare namespace pino {
labels: { [level: number]: string };
}

type PlaceholderSpecifier = 'd' | 's' | 'j' | 'o' | 'O';
type PlaceholderTypeMapping<T extends PlaceholderSpecifier> = T extends 'd'
? number
: T extends 's'
? string
: T extends 'j' | 'o' | 'O'
? object
: never;

type ParseLogFnArgs<
T,
Acc extends unknown[] = [],
> = T extends `${infer _}%${infer Placeholder}${infer Rest}`
? Placeholder extends PlaceholderSpecifier
? ParseLogFnArgs<Rest, [...Acc, PlaceholderTypeMapping<Placeholder>]>
: ParseLogFnArgs<Rest, Acc>
: Acc;

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;
<T, TMsg extends string = string>(obj: T, msg?: T extends string ? never: TMsg, ...args: ParseLogFnArgs<TMsg> | []): void;
<_, TMsg extends string = string>(msg: TMsg, ...args: ParseLogFnArgs<TMsg> | []): void;
}

interface LoggerOptions<CustomLevels extends string = never, UseOnlyCustomLevels extends boolean = boolean> {
Expand Down Expand Up @@ -886,4 +901,3 @@ export { pino as default, pino };
// `import {P} from "pino"; const log: P.Logger;`.
// (Legacy support for early 7.x releases, remove in 8.x.)
export type { pino as P };

55 changes: 54 additions & 1 deletion test/types/pino.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,63 @@ const error = log.error;

info("hello world");
error("this is at error level");
info("the answer is %d", 42);

// primative types
info('simple string');
info(true)
info(42);
info(3.14);
info(null);
info(undefined);

// object types
info({ a: 1, b: '2' });
info(new Error());
info(new Date());
info([])
info(new Map());
info(new Set());

// placeholder messages
info('Hello %s', 'world');
info('The answer is %d', 42);
info('The object is %o', { a: 1, b: '2' });
info('The json is %j', { a: 1, b: '2' });
info('The object is %O', { a: 1, b: '2' });
info('The answer is %d and the question is %s with %o', 42, 'unknown', { correct: 'order' });
info('Missing placeholder is fine %s');
Copy link
Contributor Author

@samchungy samchungy Jun 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've allowed strings with missing placeholder data but if we want to be more strict and require that they add them we can just change the type of args from ...args: ParseLogFnArgs<TMsg> | [] to ...args: ParseLogFnArgs<TMsg> .

But, this is technically valid

declare const errorOrString: string | Error;
info(errorOrString)

// placeholder messages type errors
expectError(info('Hello %s', 123));
expectError(info('Hello %s', false));
expectError(info('The answer is %d', 'not a number'));
expectError(info('The object is %o', 'not an object'));
expectError(info('The object is %j', 'not a JSON'));
expectError(info('The object is %O', 'not an object'));
expectError(info('The answer is %d and the question is %s with %o', 42, { incorrect: 'order' }, 'unknown'));
expectError(info('Extra message %s', 'after placeholder', 'not allowed'));

// object types with messages
info({ obj: 42 }, "hello world");
info({ obj: 42, b: 2 }, "hello world");
info({ obj: { aa: "bbb" } }, "another");
info({ a: 1, b: '2' }, 'hello world with %s', 'extra data');

// Extra message after placeholder
expectError(info({ a: 1, b: '2' }, 'hello world with %d', 2, 'extra' ));

// metadata with messages type errors
expectError(info({ a: 1, b: '2' }, 'hello world with %s', 123));

// metadata after message
expectError(info('message', { a: 1, b: '2' }));

// multiple strings without placeholder
expectError(info('string1', 'string2'));
expectError(info('string1', 'string2', 'string3'));

setImmediate(info, "after setImmediate");
error(new Error("an error"));

Expand Down