简体   繁体   中英

Typescript generics: Return array type to match input array by position with mapped types

I've got a function to get values from a buffer by datatype (eg U8, U16, F32, String, etc). I'm trying to figure out how to type the function so that,for example, if I pass in ['u8','u16','u16','string'] the inferred return type would be [number,number,number,string] . I found a similar question and followed the same pattern, but cannot get the result I need.

Here's the relavent portion of the code:

type BytesTypeMap = {
  U8: number,
  U16: number,
  U32: number,
  String: string,
  Text: string,
}
export type BytesType = keyof BytesTypeMap;
type BytesTypeMapped<T> = T extends BytesType ?  BytesTypeMap[T] : never;
type BytesTypeArray<T extends BytesType[]> = {
  [K in keyof T]: BytesTypeMapped<T[K]>;
}

export class MessageBuffer {

  read<T extends BytesType[]>(types:T):BytesTypeArray<T>{
    return types.map(type=>{
      const method = `read${type}` as const;
      return this[method](); // Typescript error: Type '(string | number)[]' is not assignable to type 'BytesTypeArray<T>'
    });
  }

  // ... methods matching those created with the template string above...
}

// GOAL
// ... create an instance of the class, etc.
messageBufferInstance.read(["U8","U16","String"]);
// Inferred response type should be [number,number,string]
// but is instead (number|string)[]

The compiler is not smart enough to understand that how map() converts a generic tuple of one type to a tuple of a mapped type inside the implementation of read() . The standard library's typing for the Array.prototype.map() method is

interface Array<T> {
  map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
}

which only maps arrays-to-arrays and not tuples-to-tuples... and especially not tuples-of- BytesType -to-tuples-of-related- BytesTypeMapped . And such a signature would be so specific to this particular call of map() that even trying to come up with it and merge it into the Array interface would be wasted effort.

Instead, I recommend accepting that the compiler is not up to the task of verifying type safety here, by using a type assertion to explicitly tell the compiler that you are taking responsibility for the types being correct:

read<T extends BytesType[]>(types: [...T]): BytesTypeArray<T> {
  return types.map((type: BytesType) => {
    const method = `read${type}` as const; // assuming we're using TS4.1+
    return this[method]();
  }) as BytesTypeArray<T>;
}

Notice how we are returning that as BytesTypeArray<T> . It's close enough to (string | number)[] .

Aside: I don't think ​`read${type}` as const will work before TS4.1 introduces microsoft/TypeScript#40707 . That's coming out soon so I'll leave it.

So that takes care of the implementation side of the function. Now to the caller's side:


The other piece of this is getting

const resp = messageBufferInstance.read(["U8", "U16", "String"]);

to be inferred as [number, number, string] and not (string | number)[] . We can get that to happen by changing the read() method signature to give the compiler a hint that T should be a tuple if possible instead of being widened to an array when you call read() .

There are different ways to do this, and before TS 4.0 introduced variadic tuple types , you had to do it this way (see microsoft/TypeScript#27179 )

// read<T extends BytesType[] | [BytesType]>(types: T): BytesTypeArray<T> {

where T 's constraint featured a tuple type, but now you can do it like this:

read<T extends BytesType[]>(types: [...T]): BytesTypeArray<T> {

where the types parameter is a variadic tuple spread from T . Either way should work from the caller's perspective:

const resp = messageBufferInstance.read(["U8", "U16", "String"]);
resp[0].toFixed(); // okay
resp[2].toUpperCase(); // okay

I prefer the [...T] method because it is easier on the implementation side.


Okay, so both the caller and the implementation should work as expected.

Playground link to code

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