It may be possible, but it's certainly not enjoyable.
This is the kind of UNPLEASANTNESS to which I am referring:
const resolveWith = (resolve, reject) => (err, data) =>
err ? reject(err) : resolve(data);
type Resolver = (err:any, data:any) => void;
type Nodelike1<A> = (x:A, resolver:Resolver)=>void;
type Nodelike2<A,B> = (x:A, y:B, resolver:Resolver)=>void;
type Nodelike3<A,B,C> = (x:A, y:B, z:C, resolver:Resolver)=>void;
function promisify <A>(fn:Nodelike1<A>, x:A):Promise<any>;
function promisify <A, B>(fn:Nodelike2<A,B>, x: A, y:B):Promise<any>;
function promisify <A, B, C>(fn:Nodelike3<A,B,C>, x:A, y:B, z:C):Promise<any>;
function promisify (fn, x, y?, z?) {
if (z != null) {
return new Promise((resolve, reject) =>
fn(x, y, z, resolveWith(resolve, reject)));
} else if (y != null) {
return new Promise((resolve, reject) =>
fn(x, y, resolveWith(resolve, reject)));
} else {
return new Promise((resolve, reject) =>
fn(x, resolveWith(resolve, reject)));
}
}
const readFile = promisify((url:string, cb) => cb(null, "Bob"), url);
If the url
passed in there is a string, it will work without a hitch. If you pass in a number, TypeScript will ask if you meant something else, because from the signature of the function it was passed, it wants a string.
As you can see, you can extend that off into the sunset, and turn it into a happy 300 line long bag full of fun with generics and going backwards through the arguments list.
As a person who prefers functional programming, you can't imagine how dirty this makes me feel when I implement a compose or a curry... They are ghastly... the thought that the types are making my code safer in this case is only true if my code was written like this in the first place, rather than appeasing the compiler.
Ideally, I would take this one step further and make promisify
return a function that expected the arguments x y and z separately from the function being passed in, so that it only looks that awkward once in the system.
But that's misery for another day.