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.