I'm trying to enforce the type argument of a decorator to be a subset of the decorated methods return type but I'm not able to solve this:
function foo<U>() {
return function<T extends U>(target: any, propertyKey: any, descriptor: TypedPropertyDescriptor<() => T>){};
}
class Bar {
baz: number = 0;
}
class Baz {
@foo<Bar>() // should be valid but it fails with 1241
@foo<null>() // should be valid but it fails with 1241
@foo<Bar | null>() // should be valid and it is valid
@foo<number>() // should fail and it does fail
qux(): Bar | null {
return new Bar();
}
}
I don't want to change the return type, and I guess this is my problem here. I just want to have a check for U
being a subset of T
.
EDIT: I'm using experimentalDecorators
, see https://tsplay.dev/w28pYm.
The problem is that you've erroneously written the constraint T extends U
, meaning that T
must be a subtype of U
. But you really want it to be the other way around, where T
is a supertype of U
. It would be nice if you could write T super U
instead, but TypeScript doesn't support such lower bound constraints. There's a longstanding open feature request for that at microsoft/TypeScript#14520 but so far it's not part of the language.
Luckily you can mostly emulate such a constraint, from the caller's point of view at least. The idea is to use a conditional type that swaps T extends U
(which isn't what you want) to U extends T
(which is what you want). Like this:
function foo<U>() {
return function <T extends ([U] extends [T] ? unknown : never)>(
target: any, propertyKey: any, descriptor: TypedPropertyDescriptor<() => T>) {
};
}
Here's how it works. If U extends T
is true, then the check [U] extends [T]
succeeds, and the constraint on T
becomes T extends unknown
which is always true because unknown
is the TypeScript top type). But if U extends T
is false, then the check [U] extends [T]
fails, and the constraint on T
becomes T extends never
, which is (almost) always false (unless T
is never
but that's unlikely to happen accidentally) because never
is the TypeScript bottom type.
Note that the conditional type is [U] extends [T] ? unknown : never
and not U extends T ? unknown : never
because the latter would be a distributive conditional type and that's not what you want, since if U
is a union type you don't want to check each member of U
against T
. Wrapping both sides of the check with the one-tuple [
+]
turns off distributivity while preserving the sense of the check.
And now you get the behavior you expect:
class Baz {
@foo<Bar>() // okay
@foo<null>() // okay
@foo<Bar | null>() // okay
@foo<number>() // error
qux(): Bar | null {
return new Bar();
}
}