Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Typescript - Add types for 'promisifed' methods

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:

  1. Manually adding all the needed methods is tedious and error prone.
  2. If these methods will change in future versions, I will have no idea as my code will keep compiling and I will suffer runtime exceptions.

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..

like image 753
Ilia Avatar asked Oct 07 '18 17:10

Ilia


3 Answers

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.

like image 95
Jacob Gillespie Avatar answered Sep 29 '22 07:09

Jacob Gillespie


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") };
like image 31
SeongChan Lee Avatar answered Sep 29 '22 08:09

SeongChan Lee


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 ...
    }
}
like image 29
Juan M. Medina Avatar answered Sep 29 '22 07:09

Juan M. Medina