Issue
function test(a: number, b: number, c: number) {}
function test1() {}
const o = { test, test1 };
type Type = typeof o;
function test2<K extends keyof Type>(key: K, ...args: Parameters<Type[K]>) {
const fn: Type[K] = o[key];
fn(...args);
// err:The expansion parameter must have a tuple type or be passed to the rest parameter.
}
this fn(...args);
err: The expansion parameter must have a tuple type or be passed to the rest parameter.
What do I need to do?
Solution
If you just want to move past the error as quickly as possible, you can use the any
type to disable type checking of fn
:
function test2<K extends keyof Type>(key: K, ...args: Parameters<Type[K]>) {
// TS can't figure out the relationship, just mark fn as any
const fn: any = o[key];
fn(...args); // okay
}
Of course you're paying for the convenience by completely giving up on compiler-verified type safety. When using any
or type assertions you take the responsibility for type checking away from the compiler.
If, on the other hand, you want the compiler to understand the logic of what you're doing, then you'll need to refactor the type of o
to something that actually represents it at the type level. The type checker doesn't understand that Parameters<Type[K]>
is an appropriate argument for a function of type Type[K]
. The type Type[K]
is seen as being constrained to a union of functions, as opposed to a single generic function type. The type checker can't abstract over the form { test(a: number, b: number, c: number): void; test2(): void }
to anything useful. The type Parameters<Type[K]>
is a generic conditional type and such types are notoriously inscrutable to the compiler. It doesn't ascribe meaning to the identifier "Parameters
", nor can it reason abstractly to deduce that.
This general issue of TypeScript being unable to deduce abstract correlations among data structure is discussed in microsoft/TypeScript#30581, where it's phrased in terms of correlated unions. And the supported solution and recommended approach to this is to refactor to a particular form of generic indexed accesses into mapped type. This is described in detail in microsoft/TypeScript#47109.
For the example code here, the requisite refactoring looks like
function test3<K extends keyof Type>(key: K, ...args: Parameters<Type[K]>) {
const _o: { [P in keyof Type]: (...args: Parameters<Type[P]>) => void } = o;
const fn = _o[key];
fn(...args); // okay
}
Here we assign o
to a new variable _o
of the mapped type { [P in keyof Type]: (...args: Parameters<Type[P]>) => void }
. That assignment succeeds because when given a non-generic value o
, the compiler evaluates the type of _o
completely and sees that they are structurally identical.
But even though they're structurally identical, they're represented in different forms. The mapped type explicitly captures the generic relationship between an arbitrary property of _o
and its function shape. Indexing into _o
with key
is seen not as a union of functions, but directly as a single function type, (...args: Parameters<Type[K]>) => void
. And this function type accepts ...args
easily.
So that's how TypeScript supports this sort of function while keeping some type safety guarantees. This particular refactoring didn't take much effort or added code, but often such refactorings can make the code significantly less understandable to the human developers who need to maintain it. It depends on your use case which of quick/easy/unsafe and slow/complex/safe is preferable.
Answered By - jcalz
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.