From ef2e45730f01171a52d1c12b225af0c43318eedf Mon Sep 17 00:00:00 2001 From: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> Date: Sat, 31 Oct 2026 15:21:48 +0000 Subject: [PATCH] fix(types): stop LabelValues distributing over label-name unions LabelValues used a naked type parameter in its conditional, so for labelNames ['a', 'b'] it became a union of single-key objects and reading labels back from get() failed to compile (values[0].labels.a). Wrap both sides in a tuple so the label names stay one object type. Fixes #861 Signed-off-by: sarankumar1325 <120080287+sarankumar1325@users.noreply.github.com> --- CHANGELOG.md | 1 + index.d.ts | 5 ++++- test/typescript.ts | 22 ++++++++++++++++++++++ 3 files changed, 27 insertions(+), 1 deletion(-) 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