0

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".

F-H
  • 663
  • 1
  • 10
  • 21
  • "Assuming that the possible options like `suggestionList` and `maxValue` are defined in an interface `IPropOptions`" Instead of assuming could you provide a [mre] that demonstrates what you're talking about when we paste it into our own IDEs? Otherwise I have to try to define things myself and then hoping I've done it right – jcalz Mar 21 '23 at 17:00
  • Also are you just looking for `{[K in keyof T]: IPropOptions}` which is a *mapped object type* (which has nothing whatsoever to do with `Map`)? This feels like a typo to me, if so – jcalz Mar 21 '23 at 17:02
  • @jcalz: Done. The explicit interface declaration is added. – F-H Mar 21 '23 at 17:33
  • @jcalz: Actually, that might be the solution. Among variou resources, I was looking at [that other quetion](https://stackoverflow.com/questions/54438012) and maybe got confused about using `in` directly with a type rather than `keyof `. – F-H Mar 21 '23 at 17:36
  • So then I guess either this should be closed as "caused by a typo" or answered with "use a [mapped type](https://www.typescriptlang.org/docs/handbook/2/mapped-types.html) as suggested"? – jcalz Mar 21 '23 at 17:41
  • @jcalz: Thank you, as none of these options really describe the crux of the problem I had, I have taken the liberty to [post my own answer](https://stackoverflow.com/a/75805593/5206621). I will keep it unaccepted for a little while, though, in case you or someone else writes a more comprehensive answer on the concept. – F-H Mar 21 '23 at 19:26

1 Answers1

0

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.

F-H
  • 663
  • 1
  • 10
  • 21