Reputation: 149
I'm having a hard time figuring out how to index into a mapped type using a generic argument. Below is a minimum example of what I'm trying to accomplish.
interface JsonApiObject {
attributes: { [key: string]: any };
id: string;
type: string;
}
type Narrow<Union, Type> = Union extends { type: Type }
? Union
: never;
type Store<TStore extends JsonApiObject> = {
[TModel in TStore as TModel['type']]: {
[id: string]: TModel;
};
};
class TypedStore<TStore extends JsonApiObject> {
constructor(private readonly store: Store<TStore>) { }
getModel<T extends TStore['type']>(name: T, id: string): Narrow<TStore, T> {
return this.store[name][id];
}
}
The problem I am having is that return this.store[name][id]
is failing to compile with the following message
Element implicitly has an 'any' type because expression of type 'string' can't be used to index type 'unknown'.
No index signature with a parameter of type 'string' was found on type 'unknown'.(7053)
Any help would be greatly appreciated. Here's a TS Playground link for the example
Upvotes: 1
Views: 964
Reputation: 329783
The problem here seems to be that the compiler defers evaluation of an indexed access into a key-remapped mapped type. It doesn't spend extra processing time trying to synthesize a meaningful constraint for Store<S>[T]
; it just sees it as an opaque type which might or might not have any given properties. So Store<S>[T][string]
is a type error, and things break.
See this comment in microsoft/TypeScript#47794 for an authoritative source:
We consider an indexed access type
{ [P in K]: E }[X]
to be constrained to an instantiation ofE
whereP
is replaced withX
. However, this simplification is correct only when the mapped type doesn't specify anas
clause... While it may be desirable to perform the constraint simplification for mapped types withas
clauses, it isn't possible because we can't consistently construct a reverse mapping for theas
clause (indeed, it may not even be a 1:1 mapping).
And also this comment in microsoft/TypeScript#48626:
we can't generally simplify generic mapped types to instantiations of their template type when an
as
clause is present
So, what can be done instead? Since you're trying to get Narrow<S, T>
from Store<S>[T][string]
, maybe you should write Store<S>
in terms of Narrow
directly:
type Store<S extends JsonApiObject> = {
[T in S['type']]: {
[id: string]: Narrow<S, T>;
};
};
This is just a regular mapped type without an as
clause. Indexing into it gives us the constraint mentioned above... that is, Store<S>[T] extends {[id: string]: Narrow<S, T>}
. And then things just work:
getModel<T extends S['type']>(name: T, id: string): Narrow<S, T> {
return this.store[name][id]; // okay
}
Upvotes: 2