2

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

8
  • 2
    Unfortunately, typescript ATM does not support higher order generics, i.e. what you're trying to do with T<K>. See: stackoverflow.com/a/60008205/2967697 Commented Dec 12, 2021 at 22:49
  • 1
    So your ObjectPool will only ever have a handful of items? If no, this approach doesn't scale. If yes, then this design is overkill. Can you explain the goal? Commented Dec 12, 2021 at 23:43
  • @Inigo I am creating a declaration file for a game. The player pool consists of 81 objects. The reason I use a generic type is because it can be used for other entities. The wall pool has 2000 objects. Commented Dec 13, 2021 at 0:32
  • So you intend do do something like ObjectPool<0 | 1 | 2 | ... | 1999 | 2000, Wall>? This is why I ask. That's not what static type checking is for. It doesn't make sense. Commented Dec 13, 2021 at 1:44
  • What does Wall look like? If the only generic piece is the id property and they all share it, it's possible that you could do something a little different like this to get it to work. But I agree with others that 0 | 1 | 2 | ... | 1999 is not going to be fun for you. Even 0 | 1 | 2 is probably not great. If you intend to loop over these things via numeric index, it'll be hard to treat the compiler that such an index will be of the narrowed union-of-numbers type instead of number. Commented Dec 13, 2021 at 2:20

1 Answer 1

1

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

Sign up to request clarification or add additional context in comments.

3 Comments

Thank you for turning your comment into an extensive answer. Everything you said in the second half of your answer is completely valid. The problem is, I don't own the game. I am writing a declaration file for it and so it has to be as the developers made it. The code I have to work with is also obfuscated but the variable names dont change between updates
And how bad would it be if you just wrote type ObjectPool<T> = Record<number, T & {id: number}> or type ObjectPool<T> = Record<number, (T & {id: number}) | undefined>? I suspect it comes down to how often you index into an object pool with a numeric literal (so a developer writes pool[123]) as opposed to a nonliteral expression (like pool[i] where i is number or even keyof typeof pool).
" I suspect it comes down to how often you index into an object pool with a numeric literal (so a developer writes pool[123]) as opposed to a nonliteral expression (like pool[i] where i is number or even keyof typeof pool)." The game has a global variable with the players id so its not uncommon to see x.pool[id] everywhere. Also, the game code is structured in such a way that to obtain a player you need to somehow get an id and then find the player in the pool

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.