1

I have a little TypeScript library I use for data analysis, which involves me specifying the names of a CSV file's column headings and then reusing them quite a bit throughout my code.

I've been trying to improve my code so that these column names are inferred, so my IDE will start to offer them as autocompletion suggestions, but I'm having trouble getting that inference to work in the way I had expected.

Here's a minimal reproducible example of my problem:

interface GenericInterface<T extends string> {
    cols: {
        [key in T]: string | number;
    },
}

// I expect T to be inferred as 'NAME' | 'COUNTRY'
const fileInfoA: GenericInterface = {
    cols: {
        NAME: 'A',
        COUNTRY: 'B',
    },
};

TypeScript Playground

As the comment mentions, in this example I would have expected the generic type T to have been inferred as 'NAME' | 'COUNTRY' based on the property names of the object in my cols property. However, instead TypeScript is telling me that "Generic type 'GenericInterface' requires 1 type argument(s)".

Of course, if I do provide that string union type explicitly, the error goes away. But in some cases I am dealing with CSV files where I need to know the names of 200+ columns, and I really don't want to have to repeat myself by including both that union and the object with each of those properties.

I feel like I'm either missing something very obvious or trying to do something that TypeScript doesn't support. But nothing I've been able to find in the TypeScript documentation or by searching for answers elsewhere online has brought me to a solution.

Mark Hanna
  • 3,206
  • 1
  • 17
  • 25

1 Answers1

1

You were very close. In order to infer T you need to create extra function.

interface GenericInterface<T extends string> {
    cols: {
        [key in T]: string | number;
    },
}


const foo = <T extends string>(arg: GenericInterface<T>) => arg

// <"NAME" | "COUNTRY">(arg: GenericInterface<"NAME" | "COUNTRY">)
foo({
    cols: {
        NAME: 'A',
        COUNTRY: 'B',
    },
})

Now, T is infered as "NAME" | "COUNTRY".

You can find more information about inference on function arguments in my article

Here you can find documentation

  • 1
    Aha, so TypeScript *is* able to infer this type, but only when it's inferring it as a function argument? Thanks for the links in your code, definitely useful references. I see that `foo` in your example is a noop function, just returning what it was given but otherwise doing nothing but still treated differently by the TypeScript compiler. I don't love the idea of having to use some do-nothing code just to trick the TypeScript compiler into behaving, but if that's what I need to do then I guess I'll have to have a think about how best to structure my code. Thanks for the answer. – Mark Hanna Dec 03 '21 at 08:13
  • 1
    @MarkHanna unfortunately this is the only one way to infer `T`. If you know allowed `cols` keys upfront - you can create a discriminated union without help of extra function. I perfectly understand your point about extra function. Please see [this](https://stackoverflow.com/questions/66866683/do-we-have-any-guarantees-about-arrow-functions-optimization-in-js) answer. I don't think you should worry about performance in this case. – captain-yossarian from Ukraine Dec 03 '21 at 08:16
  • 1
    Yeah it's not performance I'm worried about as it is just me being pedantic. Because I'm dealing with configuration for static files I will always know the keys up front, but I think I'd rather go with this noop approach rather than creating the type with an extra function. I'm thinking I'll just export a function rather than a type, so the configuration code doesn't change that much, and it won't really matter that the function is just there to trick the TypeScript compiler. – Mark Hanna Dec 03 '21 at 08:22