skip to Main Content

I have some Typescript Interfaces that have many repeated, similar fields. For example:

interface Foo {
   person1Name: string;
   person1Address: string;
   person2Name: string;
   person2Address: string;
   category: string;
   department: string;
}

I’m trying to shorten that by using Typescript index signature templates which works great using things like [key: 'person${number}Name']. However, I’d like to limit the number of allow keys to say 5 ‘person’ properties and it’s not working as expected.

type AllowedIndex = 1 | 2 | 3 | 4 | 5;

interface Foo {
   [key: `person${AllowedIndex}Name`]: string;
   [key: `person${AllowedIndex}Address`]: string;
   category: string;
   department: string;
}

The above gives me the error An index signature parameter type cannot be a literal type or generic type. Consider using a mapped object type instead.ts(1337).

Is there a way to do what I’m trying to do? Or am I stuck repeating the keys the way I already am?

Here is a TS Playground Link with what I’ve tried.

2

Answers


  1. You could achieve this via something like this:

    type AllowedIndex = 1 | 2 | 3 | 4 | 5;
    
    type PersonStuff = 
       { [TKey in `person${AllowedIndex}Name`]: string } &
       { [TKey in `person${AllowedIndex}Address`]: string };
    
    interface Foo extends PersonStuff {
       category: string;
       department: string;
    }
    

    See TS Playground link.

    Login or Signup to reply.
  2. TypeScript 4.4 introduced the ability to use pattern or placeholder template literal types (as implemented in microsoft/TypeScript#40598) in index signatures. But `person${AllowedIndex}Name` isn’t a pattern template literal type. In fact, it’s not even a template literal type at all, in the sense that it gets eagerly evaluated by TypeScript to be the union of string literal types "person1Name" | "person2Name" | "person3Name" | "person4Name" | "person5Name". And you can’t use a union of string literal types in an index signature.

    Instead, you can use mapped types to make an object type whose keys are a union of string literals. That would look like {[K in `person${AllowedIndex}Name`]: string} if you want the keys to be required, or {[K in `person${AllowedIndex}Name`]?: string} if you want them to be optional. Equivalently you can use a combination of the Partial and Record utility types like Partial<Record<`person${AllowedIndex}Name`, string>>.

    Note that mapped types are their own types and you cannot add properties to them. If you want to combine several mapped types you can use an intersection. Or, if you want to end up with an interface, you can use interface extension to combine the parent mapped types (but only through a named type like Partial or Record) and your added properties. That gives

    type AllowedIndex = 1 | 2 | 3 | 4 | 5;
    
    interface Foo extends
       Partial<Record<`person${AllowedIndex}Name`, string>>,
       Partial<Record<`person${AllowedIndex}Address`, string>> {
       category: string;
       department: string;
    }
    

    which you can inspect to make sure it behaves as desired:

    type ExpandedFoo = {[K in keyof Foo]: Foo[K]};
    /* type ExpandedFoo = {
        category: string;
        department: string;
        person1Name?: string | undefined;
        person2Name?: string | undefined;
        person3Name?: string | undefined;
        person4Name?: string | undefined;
        person5Name?: string | undefined;
        person1Address?: string | undefined;
        person2Address?: string | undefined;
        person3Address?: string | undefined;
        person4Address?: string | undefined;
        person5Address?: string | undefined;
    } */
    

    Playground link to code

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search