Breaking down the issue into two parts is essential. The first part involves maintaining argument types using the decorate
function, while the second part focuses on ensuring the return type of the decorated function is void
.
To address the first part, introducing a generic argument to the decorate
function can help in capturing the actual arguments passed:
const decorate = <A extends any[]>(fn: (...a: A) => void): (...a: A) => void => {
return fn;
}
const f1 = decorate((a: number) => {console.log(a);}); //(a: number) => void;
const f2 = decorate((a: number, b: string) => {console.log(a,b);}); //(a: number, b: string) => void;
const f4 = decorate((a: number, b: number) => {console.log(a,b); return a+b;}); //(a: number, b: number) => void;
Playground Link
The second part presents a challenge, as the presence of void
in a signature does not guarantee that the function will not return a value but rather that the return value will go unnoticed by the caller. While there are safeguards against explicitly returning a value with void
, functions that return values can still be assigned to signatures requiring void
.
function x() { return 1 }
let y : () => void = x
Playground Link
However, in scenarios where the return type must strictly be void
within another function like the decorate
function, leveraging conditional types can enforce custom error handling:
const decorate = <A extends any[], R>(fn: ((...a: A) => R & (void extends R ? unknown : ["Void required allowed"]))): (...a: A) => void => {
return fn;
}
const f1 = decorate((a: number) => {console.log(a);}); //OK
const f2 = decorate((a: number, b: string) => {console.log(a,b);}); //OK
const f3 = decorate((a: number, b: number) => {console.log(a,b); return a+b;}); // Type 'number' is not assignable to type '["Void required allowed"]'.
Playground Link