I have this:
const any = (any?: any): any => any;
export type Fn = (cb: (...args: (string | number)[]) => any) => any;
const Fn: Fn = any();
const t1 = Fn((x: unknown) => x); // Allowed
const t2 = Fn(<X>(x: X) => x); // Allowed
How can i disallow unknown parameters inside the callback?
I have tried:
strictFunctionTypes
inside tsconfig.json
Exact
utility typecb
declarationNone of these have worked for me so far. So, how do I avoid callbacks without constrained arguments.
Here is what should be allowed:
const t3 = Fn((a: string, b: number) => {});
const t4 = Fn(() => {});
const t5 = Fn((x: string | number, y: string) => {});
const t6 = Fn(<X extends number>(x: X) => x);
const t7 = Fn(<X extends string, Y extends string>(x: X, y: Y) => x + y);
Currently most of these don't work either... How can I solve this issue?
TypeScript really isn't intended to help enforce constraints like this. It allows you to assign a value of type (x: X) => void
to a variable of type (y: Y) => void
whenever Y extends X
, because if a function accepts all X
values, then of course it will also accept Y
values, because Y extends X
. It's a natural consequence of the contravariance of function types in their parameter types; see Difference between Variance, Covariance, Contravariance, Bivariance and Invariance in TypeScript.
It sounds like you want TypeScript to act as a linter for your code base, preventing developers from mentioning types you don't like in certain places.
To say that you want to prevent passing in an argument of type (x: unknown) => void
to a parameter of type (x: string | number) => void
is to fight against the TypeScript type system. Such a restriction is not a matter of type safety, because it doesn't seem to have anything to do with avoiding runtime errors due to type mismatches. You can certainly fight the TypeScript type system, but any solution inside TypeScript is going to be complicated and probably fragile.
Here's one possible approach:
export type Fn = <T extends Function>(
cb: T extends (T extends (...args: infer A) => void ?
A[number] extends (string | number) ? unknown : never
: never) ? T : never
) => any;
Now Fn
is generic in the type F
of the cb
parameter. When you call Fn(cb)
, TypeScript will infer T
to be the type of cb
, and then it will check it against the complicated conditional type above. If it matches T extends (...args: infer A) => void ? A[number] extends (string | number) ? unknown : never : never
then the parameter type evaluates to T
, and since T extends T
then it is acceptable. Otherwise it evaluates to never
, and since T extends never
cannot be true for any actual cb
you pass in, then it is unacceptable.
So what's T extends (...args: infer A) => void ? A[number] extends (string | number) ? unknown : never : never
? Well, we're using conditional type inference to extract the tuple type of T
's parameters into the type parameter A
, and then checking if the union of the element types of A
(that is, A[number]
) is assignable to string | number
. If it is, then we know that all the parameters of T
are subtypes of string | number
, and the overall type evaluates to unknown
. If not, then at least one parameter of T
is not a subtype of string | number
, and the overall type evaluates to never
. So that means if the parameters are what you want, the type of the cb
parameter is T extends unknown ? T : never
, or just T
, which accepts the argument you passed in. And if any parameter isn't want you want, the type of the cb
parameter is T extends never ? T : never
, or just never
, which rejects the argument you passed in.
I did say it was complicated.
Let's test it out:
const t1 = Fn((x: unknown) => x); // disallowed
const t2 = Fn(<X,>(x: X) => x); // disallowed
Fn((x: boolean) => x); // disallowed
Fn((x: string | boolean) => x); // disallowed
const t3 = Fn((a: string, b: number) => { });
const t4 = Fn(() => { });
const t5 = Fn((x: string | number, y: string) => { });
const t6 = Fn(<X extends number>(x: X) => x);
const t7 = Fn(<X extends string, Y extends string>(x: X, y: Y) => x + y);
This is all behaving as you desired. The first four calls above are rejected, since the callbacks' parameter types involve types outside the string
or number
range... even though for t1
and t2
, there would almost certainly be no runtime errors caused by it. The next five calls are all accepted, since the callbacks' parameter types are all within the string
and number
range.