简体   繁体   中英

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

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.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM