diff --git a/CHANGELOG.md b/CHANGELOG.md index 10becdfc..236acca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ project adheres to [Semantic Versioning](http://semver.org/). - fix: Non-nullish, non-string label values are coerced to strings when a combination is first stored, so exposition escapes them; the store also keeps its own copy, so mutating the caller's object after recording no longer changes the stored series - fix: Label-less summaries report `labels: {}` in `getMetricsAsJSON()`, like other metrics (Fixes [#838](https://github.com/prometheus/client_js/issues/838)) - chore: update benchmark baseline to v0.16.1 +- fix: TypeScript `LabelValues` no longer distributes over a union of label names, so labels read back from `get()` keep every declared key (Fixes [#861](https://github.com/prometheus/client_js/issues/861)) ### Added diff --git a/index.d.ts b/index.d.ts index 6e60e8bf..4bc166e2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -332,7 +332,10 @@ export type MetricValueWithName = MetricValue & { metricName?: string; }; -type LabelValues = T extends NoLabelNameType +// The tuple wrapping stops the conditional from distributing over a union of +// label names, which would turn LabelValues<'a' | 'b'> into a union of +// single-key objects instead of one object with both keys. +type LabelValues = [T] extends [NoLabelNameType] ? Partial> : Partial>; diff --git a/test/typescript.ts b/test/typescript.ts index f55a15f4..e7ac531f 100644 --- a/test/typescript.ts +++ b/test/typescript.ts @@ -73,6 +73,28 @@ async function metricObjectTypesAreExported() { } void metricObjectTypesAreExported; +// LabelValues must not distribute over a union of label names: labels read +// back from Metric#get() carry every declared key (#861). +async function labelValuesKeepEveryLabelName() { + const labelled = new Counter({ + name: 'typescript_test_labelled_counter', + help: 'TypeScript test counter with labels', + labelNames: ['a', 'b'] as const, + registers: [registry], + }); + labelled.inc({ a: '1', b: 2 }); + + const { values } = await labelled.get(); + const a: string | number | undefined = values[0].labels.a; + const b: string | number | undefined = values[0].labels.b; + void a; + void b; + + // @ts-expect-error only declared label names are allowed. + void values[0].labels.c; +} +void labelValuesKeepEveryLabelName; + // MetricType matches the runtime strings reported in metric metadata, so a // reported type can be compared against a literal and narrowed without casts. // The switch fails to compile if the union and this member list ever drift