Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Type inference breaks when using two type mappings in one type

I have the code below.

import React from 'react';

type Button = React.ComponentType<{ size: 's' }>;
type Select = React.ComponentType<{ color: string }>;
declare const button: Button;
declare const select: Select;


type AbstractFilter = Record<string, React.ComponentType<any>>;

type Props<T extends AbstractFilter> = {
    filters: {
        [P in keyof T]: {
            props: React.ComponentProps<T[P]>;
            component: T[P];
        }
    };
    values: Record<string, string>;
}

const Filter = <T extends AbstractFilter>(props: Props<T>) => {
    return <></>;
}

<Filter
    filters={{
        'hi': {
            component: button,
            props: { size: 'sf' }, // Here we got the expected TypeScript error
        },
        'ddjd': {
            component: select,
            props: { color: 'd' }
        },
    }}
    values={{
        'hi': 'some string',
        'ddjd': 'another string',
    }}
/>

Everything works as expected, but when I make values type as

values: { [P in keyof T]: string } // 'values' is an object with the same keys as filters

the type inference of filters prop breaks. props became any type. And one more interesting thing: If I create nonexisting key on values, all expected TypeScript errors appears again:

<Filter
    filters={{
        'hi': {
            component: button,
            props: { size: 'sf' }, // A TypeScript error again,
        },
        'ddjd': {
            component: select,
            props: { color: 'd' }
        },
    }}
    values={{
        'hi': 'some string',
        'ddjd': 'another string',
        'sdsdsd': 'anything', // Expected TypeScript error
    }}
/>

Why did I get such behavior? And what can I do to fix it?

like image 816
Evgen S Avatar asked Nov 14 '25 15:11

Evgen S


1 Answers

The way you've set things up, TypeScript is inferring T from all usages of it in Props (including the keyof T in values).

You can use NoInfer to prevent this from happening:

type Props<T extends AbstractFilter> = {
    filters: {
        [P in keyof T]: {
            props: React.ComponentProps<T[P]>;
            component: T[P];
        }
    };
    values: { [P in keyof NoInfer<T>]: string }
    //                    ^^^^^^^
}

(Playground)


What Was The Problem?

The way type info is displayed when you use JSX syntax makes it hard to tell what's going on, so let's use normal function call syntax instead to investigate this (rather than <Filter filters={…} values={…} />, we'll write Filter({ filters: …, values: … })).

Hover over Filter( in this playground without the NoInfer and notice that T is inferred as AbstractFilter. Conceptually, the type checker tries to fill in T with ever-wider types until all type errors go away or it hits the constraint (AbstractFilter).

Props<AbstractFilter> reduces to this:

{
    filters: Record<string, {
        props: any;
        component: React.ComponentType<any>;
    }>;
    values: Record<string, string>;
}

And since props is any, it's no surprise that you're allowed assign it arbitrary values. That's why in this case T can be inferred as AbstractFilter without any type errors showing up.

Using NoInfer makes the inference algorithm stop earlier (after considering only all non-NoInfer usages of Ts), resulting in the error you want.

like image 78
Matt Kantor Avatar answered Nov 17 '25 09:11

Matt Kantor



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!