Skip to content

TypeScript: LabelValues distributes over label-name unions, so labels from get() lose all but one key #861

Open

Description

@macourteau

In 0.16.x, LabelValues is a distributive conditional type:

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

Because T is a naked type parameter, TypeScript distributes the conditional over each member of a label-name union. For a metric with labelNames: ['a', 'b'], LabelValues<'a' | 'b'> becomes Partial<Record<'a', string | number>> | Partial<Record<'b', string | number>> rather than a single object with both keys.

Writes still compile, but reading labels back does not, e.g. from get():

import { Counter } from '@prometheus-io/client';

const c = new Counter({ name: 'x_total', help: 'x', labelNames: ['a', 'b'] as const });
c.inc({ a: '1', b: '2' });
const { values } = await c.get();
values[0].labels.a;
// error TS2339: Property 'a' does not exist on type
// 'Partial<Record<"a", string | number>> | Partial<Record<"b", string | number>>'.

This compiled with prom-client 15.1.3, where LabelValues<T> was Partial<Record<T, string | number>>.

Wrapping both sides in a tuple disables distribution and keeps the never (no labels) case working:

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

(As a side effect, the current form also resolves to never rather than Partial<Record<string, never>> when T is never, since a distributive conditional over never yields never.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions