I have a very simple module that wraps the "fs" module. This module simply "promisifies" all of "fs" methods and exports the new object:
fsWrapper.ts
import Promise from "bluebird";
import * as fs from "fs";
const fsWrapper = Promise.promisifyAll(fs);
export = fsWrapper;
Now I can use this wrapper instead of "promisifiying" the "fs" module inside every caller module, like so:
main.ts
import fsWrapper from "./fsWrapper";
function test(): void {
fsWrapper.readFileAsync("tst.txt", "utf8")
.then((data: Buffer) => {
console.log("data:", data.toString());
})
}
This of course doesn't work with typescript since "fs" doesn't hold the readFileAsync
method and I receive a compiler error.
While searching of ways of properly typings this wrapper, I found the following typescript issue.
Using its approach, I can create my own fsWrapper.d.ts
, in which I need to manually add the *Async methods, similar to the following:
fsWrapper.d.ts
import Promise from "bluebird";
declare module "fs" {
export function readFileAsync(path: string, options: { encoding?: string | null; flag?: string; } | string | undefined | null) : Promise<Buffer>;
...
}
The problem here is:
I know that util.promisify
is properly typed, and therefore thought of somehow extending "fs" with these new methods, similar to the following:
fsWrapperTest.ts
import * as fs from "fs";
import { promisify } from "util";
let fsWrapper = fs;
fsWrapper.readFileAsync = promisify(fs.readFile);
export = fsWrapper;
But this outputs an error that indicates that I can't extend existing modules:
error TS2551: Property 'readFileAsync' does not exist on type 'typeof "fs"'. Did you mean 'readFileSync'?
Is there any way for me to properly type this wrapper while keeping "up-to-date" typings and working with Intellisense?
Edit:
Just to clarify, I understand how to create an object that will have all "fs" methods promisified (that was the case with the original fsWrapper.ts
above).
What i'm struggling is typing it properly for Intellisense usage. For example, running the following:
import * as fs from "fs";
import { promisify } from "util";
let fsWrapper = Object.keys(fs).reduce((p: typeof fs, v: string) => { p[v] = promisify(fs[v]); return p }, {})
Will give me a fsWrapper: {}
typing for the object.
I would like to have all of "fs" methods + the new 'promisifed' methods as its type.
EDIT - selected solution
I ended up using the declare module 'fs'
approach by extending the 'fs' module with all the *Async methods that I was using in my code.
Not ideal but it seems that there is no better alternative..
If you are using TypeScript 3.0 or newer, the improvements to mapped types and parameter tuples should make this possible. For example:
type UnpackedPromise<T> = T extends Promise<infer U> ? U : T
type GenericFunction<TS extends any[], R> = (...args: TS) => R
type Promisify<T> = {
[K in keyof T]: T[K] extends GenericFunction<infer TS, infer R>
? (...args: TS) => Promise<UnpackedPromise<R>>
: never
}
That creates a type for unwrapping a promise, a type for a generic function, and then an inferred mapped type for the promisified version that maps each key to a new value that is the promisified version of the previous function. To use that type (playground link):
const original = {
one: () => 1,
two: () => 2,
add: (a: number, b: number) => a + b
}
const promisified = original as unknown as Promisify<typeof original>
promisified.one()
promisified.add(1, 2)
And for your example, here's how that would be used:
import * as fs from "fs";
import { promisify } from "util";
let fsWrapper = Object
.keys(fs)
.reduce((p: typeof fs, v: string) => { p[v] = promisify(fs[v]); return p }, {}) as unknown as Promisify<typeof fs>
Notice that you are casting to unknown and then to Promisify<*>
- this is because by default it will consider the mapped type to be possibly a mistake.
Node.js >= v10.x started to provide already promisified functions.
You can use them via require("fs").promises
or require("fs/promises")
(>= v14.x)
https://nodejs.org/docs/latest-v10.x/api/fs.html#fs_fs_promises_api https://nodejs.org/docs/latest-v14.x/api/fs.html#fs_fs_promises_api
I use
const fs = { ...require("fs"), ...require("fs/promises") };
There is an existing thread on github exactly for this, see https://github.com/Microsoft/TypeScript/issues/8685
So, you can do something like:
import { Promise } from 'bluebird';
import * as fs from "fs";
declare module "fs" {
interface fs {
[method: string]: any;
}
}
const fsWrapper = Promise.promisifyAll(fs);
export = fsWrapper;
This is already very similar to what you were doing to get past the issue of calling the methods.
Please note that as far as I know the only way to allow full Intellisense is to do module augmentation as described in the typescript documentation https://www.typescriptlang.org/docs/handbook/declaration-merging.html
But that implies you manually writing the interface for the new methods generated by the promisified inside that module structure above. i.e.
declare module "fs" {
interface fs {
readFileAsync(... ),
writeFileAsyng(... ),
etc ...
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With