Home > Net >  How to express a generic constraint to have a dynamic property of a specific type
How to express a generic constraint to have a dynamic property of a specific type

Time:09-13

I'm building a library that provide a generic utility function.

This function should do something with one of the property of the incoming object, but the property name is also a parameter provided by the user.

To simplify, imagine :

type T1 = {
    id: string;
    foo: number;
    bar: string[];
}
const sampleData1: T1 = { bar: ["a"], foo: 42, id: "myid" }
const doSomething = <F>(somearg: F, path: keyof F) => {
    console.log(somearg[path]);
}

doSomething(sampleData1, "id");

The output is what I expect : "myid".

However, I want only string to be used in my function. But the actual code will also work with:

type T3 = {
    wrong: number;
    value: number;
}
const sampleData3 : T3 = { value : 100, wrong : 5 }

const doSomething = <F>(somearg: F, path: keyof F) => {
    console.log(somearg[path]);
}

doSomething(sampleData3, "wrong");

This code works and emits 5. But my actual function should only works with strings.

Is there a way to express the F generic type to have the dynamically name property be only a string ?

I tried :

const doSomething = <F extends { [path] : string }>(somearg: F, path: keyof F) => {

    console.log(somearg[path]);

}

but the syntax is invalid :

A computed property name in a type literal must refer to an expression whose type is a literal type or a 'unique symbol' type.
Cannot find name 'path'.

How to fix that ?

FYI : I run TS 4.5.5. My actual function is in a dependency, and the consuming code is present in multiple apps, with very different business needs.

The utility function may also be called several times on the same object, but with different property name.

Full repro on TS playground

CodePudding user response:

The syntax to index an object type in this case is { [key in KeyType]: valueType }.

In your case, you want to constrain F so that the path has a string value, but you do not care about the rest.

You can use 2 generic types: 1 for the path, and 1 for the object:

const doSomething = <P extends string, F extends { [p in P]: string }>(somearg: F, path: P) => {}

It correctly rejects the 3rd sample:

doSomething(sampleData1, "id");
doSomething(sampleData2, "key");
doSomething(sampleData3, "wrong"); // Error: Type 'number' is not assignable to type 'string'.

Playground Link

  • Related