How can I restrict fields in an interface to match the fields of a generic parameter?

302 Views Asked by At

I would like to define an interface (or other type?) that describes another object (whose type should be provided as a generic parameter).

Basically, it should look something like this:

interface IDescriptor<T extends object> {
    propSet1?: (keyof T)[];
    
    propSet2?: (keyof T)[];
}

So far, so good - now I can only list properties that are actually found in whichever type is provided for type parameter T in propSet1 and propSet2.


As a simple example, imagine a type Person:

class Person {
    firstName?: string;
    
    lastName?: string;
    
    age?: number;
}

Now, in type IDescriptor<Person>, propSet1 and propSet2 can only contain either of the values 'firstName', 'lastName', and 'age'.


However, I now require IDescriptor to optionally contain some additional information on each of the fields from T. Concretely, I would like to specify the following object literal:

let desc: IDescriptor<Person> = {
    propSet1: ['firstName', 'lastName'],
    propSet2: ['lastName', 'age'],
    propInfo: {
        lastName: {
            suggestionList: 'surnames'
        },
        age: {
            maxValue: 120
        }
    }
};

In the above example, the possible options are defined in the following interface:

interface IPropOptions {
    suggestionList?: string;
    maxValue?: number;
}

Now, how do I declare the propInfo property?

The two approaches I have thought of fail for different reasons:

I have tried uing the in operator:

propInfo?: {
    [key in T]: IPropOptions;
};

This fails with the message

TS2322: Type 'T' is not assignable to type 'string | number | symbol'.
  Type 'object' is not assignable to type 'string | number | symbol'.

I have tried using a keyof expression:

propInfo?: {
    [key: keyof T]: IPropOptions;
};

Unfortunately, this fails with the message

TS1337: An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.

Then, I have also tried using a map, as suggested:

propInfo?: Map<keyof T, IPropOptions>

This in itself appears to be valid, but here, I don't know how to specify the value as an object literal.

Is there any way to solve this as desired? I am especially unsure as there appear to be subtle differences in what is or is not allowed depending on e.g. whether I declare something as an interface or as a "type".

1

There are 1 best solutions below

0
F-H On

The comments by user jcalz helped me to find the answer:

In TypeScript, a "mapped object type" is actually a set term denoting a specific language concept, not just a generic way to refer to the standard map types, as I had mistakenly thought.

Thus, the error message

Consider using a mapped object type instead.

was actually pointing out the right syntax to use for the concept that I thought I was already using:

propInfo?: {
    [key in keyof T]: IPropOptions;
};

appears to do what I am looking for.