Skip to content
This repository was archived by the owner on Sep 1, 2026. It is now read-only.
This repository was archived by the owner on Sep 1, 2026. It is now read-only.

Generic type parameter inferred differently across two call-site positions with opposite variance (narrow type vs union) #4683

Closed

Description

Steps to reproduce

In the following code, hover on data in TopMetricsTable

import * as React from 'react';

// --- Base type and its structural superset ---
type Metric = {
    metricKey: string;
};

type AdditionalMetricDetails = {
    initialized: boolean;
};

type CardinalityInfo = {
    cardinality: number;
};

type MetricWithCardinality = Metric & Required<CardinalityInfo>;
type EnrichedMetricWithCardinality = Metric & AdditionalMetricDetails & Required<CardinalityInfo>;
// ^ EnrichedMetricWithCardinality is a strict superset of MetricWithCardinality —
//   every Enriched instance already satisfies MetricWithCardinality.

// --- Table/Column machinery, mirroring a react-table-style generic component ---
type TableInstance<D extends object> = {
    defaultColumn?: Partial<Column<D>>;
};

type CellProps<D extends object, V = any> = TableInstance<D> & {
    value: V;
    row: { original: D };
};

type Column<D extends object, V = any> = {
    Header: string;
    accessor: keyof D;
    Cell?: (props: CellProps<D, V>) => React.ReactNode;
};

type TableColumn<D extends object> = Column<D>;

interface TableProps<D extends object> {
    data: D[] | undefined;
    columns: TableColumn<D>[];
}

function Table<D extends object>(props: TableProps<D>): React.ReactElement {
    return <div>{props.data?.length}</div>;
}

// --- The actual usage ---
const columns: TableColumn<MetricWithCardinality>[] = [
    {
        Header: 'Metric Key',
        accessor: 'metricKey',
        Cell: ({ value }: CellProps<MetricWithCardinality, string>) => value,
    },
];

function TopMetricsTable(props: {
    metrics: (MetricWithCardinality | EnrichedMetricWithCardinality)[];
}) {
    return <Table columns={columns} data={props.metrics} />;
}

Behavior with [email protected]

Hover shows: (property) TableProps<MetricWithCardinality>.data: MetricWithCardinality[] | undefined

D is inferred as MetricWithCardinality — resolved from the columns prop (a contravariant position, since D appears inside Cell's function parameter), not from data.

Behavior with tsgo

Hover shows: (property) TableProps<EnrichedMetricWithCardinality | MetricWithCardinality>.data: (EnrichedMetricWithCardinality | MetricWithCardinality)[]

D is inferred as the full union instead — resolved from data (a covariant position). Since EnrichedMetricWithCardinality is a strict superset of MetricWithCardinality, both resolutions are arguably sound (the union collapses to MetricWithCardinality for assignability purposes), but the two compilers pick different candidates when resolving the same type parameter from two opposite-variance positions in the same call.

In real code, this difference in which candidate wins can cascade into a real TS2322 (columns declared for the narrow type fails against the wider inferred D) even though [email protected] accepts the exact same code

Activity

  1. Andarist commented on Jul 21, 2026

    @Andarist
    Contributor

    bisects to #4389

  2. ahejlsberg commented on Jul 21, 2026

    @ahejlsberg
    Member

    This was fixed by #4591 which is now in main.

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

Metadata

Metadata

Labels

duplicateThis issue or pull request already exists

Type

No type

Projects

No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions