From 2593651c3abc6d92290fbde1b4102bd2839cdc4a Mon Sep 17 00:00:00 2001 From: Christopher Pruijsen Date: Tue, 29 Sep 2026 04:48:44 +0100 Subject: [PATCH] fix(types): stop LabelValues distributing over label-name unions A naked T extends never splits multi-label metrics into a union of one-key objects, so reading labels back from get() keeps only one key. Fixes #861 Signed-off-by: Christopher Pruijsen --- CHANGELOG.md | 1 + index.d.ts | 2 +- test/typescript.ts | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 765b92b7..4603f026 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ project adheres to [Semantic Versioning](http://semver.org/). ### Changed +- fix: LabelValues no longer distributes over label-name unions, so `get()` labels keep every declared key (Fixes [#861](https://github.com/prometheus/client_js/issues/861)) - Update Prettier and lint-staged - Organized default metrics - perf: Histogram rendering builds its export list straight from the store iterator instead of an intermediate array. Faster at high series counts on Node 24 and 26, can be slightly slower on Node 22 diff --git a/index.d.ts b/index.d.ts index 6e60e8bf..11f87226 100644 --- a/index.d.ts +++ b/index.d.ts @@ -332,7 +332,7 @@ export type MetricValueWithName = MetricValue & { metricName?: string; }; -type LabelValues = T extends NoLabelNameType +type LabelValues = [T] extends [NoLabelNameType] ? Partial> : Partial>; diff --git a/test/typescript.ts b/test/typescript.ts index f55a15f4..3796c219 100644 --- a/test/typescript.ts +++ b/test/typescript.ts @@ -14,6 +14,7 @@ import { Counter, + Gauge, Pushgateway, Registry, MetricObject, @@ -107,3 +108,40 @@ async function metricTypeMatchesRuntimeStrings() { void MetricType.Counter; } void metricTypeMatchesRuntimeStrings; + +// LabelValues must not distribute over a label-name union. A naked +// `T extends never` splits Counter<'method' | 'status'> into a union of +// one-key objects, so reading labels back from get() keeps only one key +// (#861). Tuple-wrapping both sides keeps every declared name, and the +// no-label `never` case still resolves to Partial>. +const twoLabelCounter = new Counter({ + name: 'typescript_two_label_counter', + help: 'TypeScript two-label counter', + labelNames: ['method', 'status'] as const, + registers: [registry], +}); +const twoLabelGauge = new Gauge({ + name: 'typescript_two_label_gauge', + help: 'TypeScript two-label gauge', + labelNames: ['method', 'status'] as const, + registers: [registry], +}); + +async function twoLabelGetKeepsBothKeys() { + const counterSnapshot = await twoLabelCounter.get(); + const method: string | number | undefined = + counterSnapshot.values[0].labels.method; + const status: string | number | undefined = + counterSnapshot.values[0].labels.status; + void method; + void status; + + const gaugeSnapshot = await twoLabelGauge.get(); + const gaugeMethod: string | number | undefined = + gaugeSnapshot.values[0].labels.method; + const gaugeStatus: string | number | undefined = + gaugeSnapshot.values[0].labels.status; + void gaugeMethod; + void gaugeStatus; +} +void twoLabelGetKeepsBothKeys;