Home > OS >  TypeScript interface mapped property generic type
TypeScript interface mapped property generic type

Time:12-14

I'm trying to make this work

interface ObjectPool<Ids, T> {
  pool: {
    [K in Ids]: T<K>;
  };
};

interface Player<Id> {
  id: Id;
}

let playerPool: ObjectPool<0 | 1 | 2, Player>;

so that

playerPool[0].id === 0;
playerPool[1].id === 1;
playerPool[2].id === 2;
// playerPool[3] error

but typescript says I need a generic parameter at Player in let playerPool: ObjectPool<0 | 1 | 2, Player>; so I tried let playerPool: ObjectPool<0 | 1 | 2, Player<_>>; but that doesn't work too

CodePudding user response:

If you write T<K>, then T needs to be some particular type operation (e.g., type T<K> = ... or interface T<K> { ... or class T<K> ...). There's no way to write T<K> where T is a generic type parameter. That would require so-called higher-kinded types, of the sort requested in microsoft/TypeScript#1213, and TypeScript has no direct support for that.

You could step back and try to think of exactly what you want to do, and if there's any way to represent it without needing higher kinded types. If all you want is for ObjectPool<P, T> to have all property keys in P, and for each such key K, you want the property value to have an id property equal K in addition to some other properties specified by T, then you can separate out the id part in the definition so that T is just a regular type. For example:

type ObjectPool<P extends PropertyKey, T> =
  { [K in P]: { id: K } & T };

Now you could define Player without making it generic:

interface Player {
  id: number,  // <-- you don't necessarily need this anymore
  name: string,
}

And now an ObjectPool<0 | 1 | 2, Player> should behave as desired:

function processPlayerPool(playerPool: ObjectPool<0 | 1 | 2, Player>) {
  playerPool[0].id === 0;
  playerPool[1].id === 1;
  playerPool[2].id === 2;
  playerPool[2].name;
  playerPool[3] // error
}

You can then define other types to use instead of Player and use them too:

interface Wall {
  location: string
  orientation: string
}

function processSmallWallPool(wallPool: ObjectPool<0 | 1, Wall>) {
  wallPool[0].location // okay
  wallPool[0].id === 0; // okay
  wallPool[1].orientation // okay
  wallPool[2] // error
}

You mentioned in a comment that you have 2,000 Wall objects in the pool. That's a lot of elements to put in a union, but sure, you could do it (code generation is going to be easier than trying to convince the compiler to compute it):

// console.log("type WallIds = "   Array.from({ length: 2000 }, (_, i) => i).join(" | "));
type WallIds = 0 | 1 | 2 | 3 | 4 | // ✂ SNIP! 
  | 1995 | 1996 | 1997 | 1998 | 1999

And then ObjectPool<WallIds, Wall> will also behave as desired:

function processWallPool(wallPool: ObjectPool<WallIds, Wall>) {
  wallPool[214].location
  wallPool[100].id // 100
  wallPool[1954].orientation
  wallPool[2021] // error
}

Please note though that the compiler really can't do much analysis on a union of numeric literals. You might have more trouble than you expected with this. If you try to loop over the elements of wallPool with a numeric index i, the compiler will complain:

  for (let i = 0; i < 2000; i  ) {
    wallPool[i] // error! 
  //~~~~~~~~~~~ 
  // No index signature with a parameter of type 'number' 
  // was found on type 'ObjectPool<WallIds, Wall>'
  }

It has no idea that i is guaranteed to be a value of type WallIds in that loop. It infers number, and you can't index into wallPool with any old number. It needs to be a value of the WallIds union. You could assert that i is a WallIds:

  for (let i = 0 as WallIds; i < 20000; i  ) {
    wallPool[i] // no error, but, 20000 is an oopsie
  }

but, as shown above, you run into the problem that the compiler can't understand that i might make i no longer a valid WallIds, as explained in this comment of microsoft/TypeScript#14745.

If you're only ever going to be indexing into WallPool with a numeric literal value, like wallPool[123] or wallPool[1987], then that's fine. But as soon as you start storing and manipulating indices of type number, you will likely hit a roadblock with this approach. It might still be worth it to you, but it's important to be aware of it.

Playground link to code

  • Related