Reputation: 6172
I am trying to write a Default
decorator, so that this would work:
class A {
@Default(() => new Date())
test: Date;
}
I am trying to achieve strong typing, so that the decorator parameter is either (1) of the decorated property type, or (2) a parameterless function returning a value of the decorated property type.
I tried the following:
type DefaultParam<T> = T | (() => T);
export function Default<K extends string, T extends Record<K, V>, V>(param: DefaultParam<V>): (t: T, p: K) => void {
return (target: T, propertyKey: K) => {
// Probably irrelevant
};
}
This, however, fails with:
Argument of type 'A' is not assignable to parameter of type 'Record<string, Date>'.
Index signature is missing in type 'A'.
Specifying the type parameters, however, works as expected:
class A {
@Default<'test', A, Date>(() => new Date())
test: Date;
}
Is there a way to write the decorator so that inference works as expected, to avoid explicitly specifying the parameter in the Default
call?
Upvotes: 1
Views: 540
Reputation: 328262
I'd say the main problem here is that the curried decorator function can't infer a specification for the K
type parameter (or the T
type parameter) when you use it. If you have a curried generic function with a type like:
declare const curryBad: <T, U>(t: T) => (u: U) => [T, U]
the compiler will try to infer T
and U
when you call it. Let's say you call curryBad(1)
. The 1
value causes T
to be inferred as number
. But there's no value of type U
here, and thus type inference fails for U
and it becomes unknown
. The result of curryBad(1)
is therefore (u: unknown) => [number, unknown]
:
const bad = curryBad(1)(""); // [number, unknown]
While it's not inconceivable that a compiler might theoretically defer inferring U
until the returned function is called, that's not the way it works in practice. Instead, you can just write the function's signature that way to begin with: don't declare U
as a parameter of the initial function; declare it as a parameter of the returned function instead:
declare const curryGood: <T>(t: T) => <U>(u: U) => [T, U]
Now calling curryGood(1)
will return a value of type <U>(u: U) => [number, U]
, which is probably what you want:
const good = curryGood(1)(""); // [number, string]
With that in mind, I'd suggest moving your K
parameter. Also, I'm not sure you really need T
to be its own generic type; Record<K, V>
might be good enough. But if you find you do need it, it should also be moved along with K
:
export function Default<V>(
param: DefaultParam<V>): <K extends PropertyKey>(t: Record<K, V>, p: K) => void {
return <K extends PropertyKey>(target: Record<K, V>, propertyKey: K) => {
};
}
And now your decorator should hopefully work as desired:
class Good {
@Default(() => new Date()) // no error
test!: Date;
}
class Bad {
@Default(123) // error! Date is not number
test!: Date;
}
Okay, hope that helps; good luck!
Upvotes: 2