Как вы упомянули, вам потребуется проверка времени выполнения. Система типов TypeScript stati c: стирается при компиляции в JavaScript, поэтому обе строки store.get<GameA>(0)
и store.get<GameB>(0)
компилируются в store.get(0)
. Это означает, что у них нет возможности вести себя иначе, чем друг от друга.
Вместо этого вы можете изменить свой get()
метод, чтобы он принимал некоторую информацию о типе как фактический параметр времени выполнения, а не только как тип параметр. Проще всего передать конструктор экземпляра класса, который вы пытаетесь получить:
get<T extends AllGames>(ctor: new (...args: any) => T, idx: number): T {
const item = this.store[idx];
if (typeof item === 'undefined') {
throw new Error("Index out of bounds");
}
if (!(item instanceof ctor)) {
throw new Error("Item is the wrong type");
}
return item;
}
Вызов не сильно отличается от предыдущего:
console.log('First item:', store.get(GameA, 0).name); // Anton
console.log('Second item:', store.get(GameA, 1).name); // runtime error
но теперь вы получите ошибку времени выполнения, как только попытаетесь get()
что-то неправильного типа. Однако я не уверен, что это именно то, что вы ищете.
Возможно, вы хотите, чтобы компилятор выдавал ошибку, когда вы get()
что-то не того типа, или, что еще лучше, чтобы компилятор знал какой тип будет при звонке на get()
. Это означает, что когда вы вызываете store.add(new GameA('Anton'))
, компилятор изменяет тип store
, чтобы помнить, что его аргумент с индексом 0
- это GameA
, а не GameB
. Это возможно с функциями утверждения , хотя использовать их несколько болезненно:
type Next = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
class GameCollection<N extends number = 0, A extends Record<number, AllGames> = {}> {
store: Array<AllGames> & A;
constructor() {
this.store = [] as any;
}
add<T extends AllGames>(g: T): asserts this is GameCollection<Next[N], A & Record<N, T>> {
this.store.push(g)
}
get<N extends keyof A>(idx: N): A[N] {
return this.store[idx];
}
}
Здесь мы удалили все проверки времени выполнения и вместо этого add()
сузили тип store
. Мне пришлось собрать Next
, чтобы компилятор мог понять, что каждый add()
сохраняет значение в следующем индексе; возможно, есть способ лучше, но это всего лишь набросок.
Итак, вот как мы будем его использовать. Самая болезненная часть функций утверждения заключается в том, что вам нужно добавлять явные аннотации в местах, которые вам не нужно было раньше:
const store: GameCollection = new GameCollection()
// ------> !!!!!!!!!!!!!!!!
// this annotation is NECESSARY;
Если вы проверяете store
, это тип :
// const store: GameCollection<0, {}>
Затем посмотрите, что происходит после каждого add()
:
store.add(new GameA('Anton'));
store; // const store: GameCollection<1, Record<0, GameA>>
store.add(new GameB(42));
store; // const store: GameCollection<2, Record<0, GameA> & Record<1, GameB>>
Итак, теперь store
можно использовать следующим образом:
console.log('First item:', store.get(0).name); // okay
console.log('Second item:', store.get(1).name); // error!
// ------------------------------------> ~~~~
// no name on GameB
store.get(2); // error!
// -----> ~
// 2 is not assignable to 0 | 1
Я думаю, что это изящно, но в целом это, вероятно, не самая лучшая идея, если только вы не планируете создавать и использовать эти коллекции внутри связанных частей кода TypeScript. Если кто-то передаст вам неизвестный GameCollection
, то компилятор ничего не знает о его содержимом, и вы снова застрянете, выполняя проверки во время выполнения.
В любом случае, надеюсь, что это поможет; удачи!
Детская площадка ссылка на код