Home > Blockchain >  TypeScript type for a function with a return type specified by an input parameter
TypeScript type for a function with a return type specified by an input parameter

Time:07-09

I’m trying to specify a TypeScript type for a function that takes an input and an optional outputType parameter which determines the return type. If no outputType is specified the return type matches typeof input. The return value may also be a Promise of that type.

For example,

type Decrypter = (options: {
  input: string | Buffer | stream.Readable;
  outputType?: "string" | "buffer" | "stream" | undefined;
}) => X | Promise<X>;

Where X would be string, Buffer, or stream.Readable depending on the value of outputType, or the same type as options["input"] if outputType is undefined.

I feel like there is something that can be done with generics, extends, and possibly infer, but I can’t quite get the right incantation!

Any ideas?

Below is an example of a basic implementation:

const decrypter: Decrypter = async ({ input, outputType }) => {
  let encrypted: string;
  if (typeof input === "string") {
    encrypted = input;
    outputType ??= "string";
  } else if (Buffer.isBuffer(input)) {
    encrypted = input.toString("utf8");
    outputType ??= "buffer";
  } else if (Readable.isReadable(input)) {
    const buffers = [];
    for await (const buffer of input as Readable) {
      buffers.push(buffer);
    }
    encrypted = Buffer.concat(buffers).toString("utf8");
    outputType ??= "stream";
  } else {
    throw new Error("Invalid input");
  }

  const decrypted = encrypted.split("").reverse().join("");

  switch (outputType) {
    case "string":
      return decrypted;
    case "buffer":
      return Buffer.from(decrypted);
    case "stream":
      return Readable.from(decrypted);
    default:
      throw new Error("Invalid outputType");
  }
};

I’m using Node.js v18.4.0, TypeScript v4.7.4.

CodePudding user response:

The first thing we need is a map which maps from the string literal type to the return type.

type TypeMap = {
  string: string
  buffer: Buffer
  stream: stream.Readable
}

For both input and output we add the generic types I and O to capture the type of the object passed to the function. We also constrain both generic types to their respective allowed inputs. We add never as the default type for O if a type for outputType was not provided.

type Decrypter = <
  I extends TypeMap[keyof TypeMap], 
  O extends keyof TypeMap = never
>(options: {
  input: I;
  outputType?: O;
}) => DecryptorReturn<O, I>;

For the return type we check if any type was provided for O. If yes, we can use the TypeMap to lookup the type.

type DecryptorReturn<O extends keyof TypeMap, I> = [O] extends [never]
  ? I | Promise<I> 
  : TypeMap[O] | Promise<TypeMap[O]>

Playground


Now some tests to see if it's working:

const fn: Decrypter = {} as any

const res1 = fn({ input: "abc" })
//    ^? const res1: "abc" | Promise<"abc">

const res2 = fn({ input: "abc", outputType: "buffer" })
//    ^? const res2: Buffer | Promise<Buffer>

Looks good to me.


When you writing functions with complex generic types, TypeScript will always have problems understanding the logic of your function implementation. For example, you try to assign a string to outputType. But since outputType is of type O TypeScript will net let you assign anything to it.

So have some short-comings when trying to type the function itself. First of all, I would give input and outputType explicit types which are not generic so that you can assign things to them.

(async ({ input, outputType }: {input: TypeMap[keyof TypeMap], outputType: keyof TypeMap}) => { ... }

TypeScript will now have problems with assigning this function to the type Decrypter because it is not generic. I would use a simple type assertion to silence this error.

const decrypter: Decrypter = (async ({ input, outputType }: {input: TypeMap[keyof TypeMap], outputType: keyof TypeMap}) => {
  /* ... */
}) as Decrypter

Playground

  • Related