Reputation: 2182
So in the playground I can easily write a property decorator that is type-safe. Meaning that it requires the property its decorating to have a certain type.
Here's the code in case you don't want to check out the playground right now.
function decorate<T extends Record<K, number>, K extends string | symbol>(target: T, key: K) {
// The implementation of this function doesn't matter.
// Only its type definition matters for now.
}
class Foo {
// Try changing 5 to a string like "hello"
// You'll see that Typescript will rightly complain.
@decorate public bar = 5
}
However when I try implementing that into my actual project, which uses more complicated generics, then it doesn't work anymore. Here's my code:
export const subcollection = <SubModel extends Model, SubModelClass extends Constructable<SubModel>, Target extends Model & Record<Key, SubModel[]>, Key extends string | symbol>(subModelClass: SubModelClass) => (target: Target, key: Key): void => {
// Again, the implementation doesn't matter.
}
I use this decorator in model classes that use Firestore as the backend database. It could look something like this.
class Article {
@subcollection(Comment) public comments!: Comment[]
}
But for some reason, when the types become this complex, I get the following error:
Argument of type 'Article' is not assignable to parameter of type 'Model & Record<string | symbol, Comment[]>'.
Type 'Article' is not assignable to type 'Record<string | symbol, Comment[]>'. Index signature is missing in type 'Article'.
So why does this happen in my real world case, but not in the playground? Am I doing something wrong and could I actually get it to work?
Upvotes: 0
Views: 990
Reputation: 3666
The difference is that TS can't infer the type of Key
from the arguments in the second case because you've templated only the factory function and not the function which actually accepts the key
argument. Because of this TS can't narrow the type for Key
and that causes it to require an index signature to satisfy Record<Key, X>
.
The solution is to move the generic parameters for Key
and Target
to the inner function so that TS can infer them from the arguments:
export const subcollection =
<SubModel extends Model, SubModelClass extends Constructable<SubModel>> (subModelClass: SubModelClass) =>
<Target extends Model & Record<Key, SubModel[]>, Key extends string | symbol> (target: Target, key: Key): void => {
// implementation
}
Upvotes: 2