diff --git a/pino.d.ts b/pino.d.ts index 87eebe38d..15717f6da 100644 --- a/pino.d.ts +++ b/pino.d.ts @@ -319,12 +319,27 @@ declare namespace pino { labels: { [level: number]: string }; } + type PlaceholderSpecifier = 'd' | 's' | 'j' | 'o' | 'O'; + type PlaceholderTypeMapping = 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]> + : ParseLogFnArgs + : Acc; + interface LogFn { - // TODO: why is this different from `obj: object` or `obj: any`? - /* tslint:disable:no-unnecessary-generics */ - (obj: T, msg?: string, ...args: any[]): void; - (obj: unknown, msg?: string, ...args: any[]): void; - (msg: string, ...args: any[]): void; + (obj: T, msg?: T extends string ? never: TMsg, ...args: ParseLogFnArgs | []): void; + <_, TMsg extends string = string>(msg: TMsg, ...args: ParseLogFnArgs | []): void; } interface LoggerOptions { @@ -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 }; - diff --git a/test/types/pino.test-d.ts b/test/types/pino.test-d.ts index 3c3d6e955..4a8f83f90 100644 --- a/test/types/pino.test-d.ts +++ b/test/types/pino.test-d.ts @@ -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'); +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"));