Skip to content
Open
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ export type MetricValueWithName<T extends string> = MetricValue<T> & {
metricName?: string;
};

type LabelValues<T extends string> = T extends NoLabelNameType
type LabelValues<T extends string> = [T] extends [NoLabelNameType]
? Partial<Record<string, never>>
: Partial<Record<T, string | number>>;

Expand Down
38 changes: 38 additions & 0 deletions test/typescript.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

import {
Counter,
Gauge,
Pushgateway,
Registry,
MetricObject,
Expand Down Expand Up @@ -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<Record<string, never>>.
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;