简体   繁体   中英

How to declare the return type of a generic arrow function to be an array and value at the same time in typescript?

Consider a function which takes an array, property name and an arrow function as arguments. What I want to achieve is, filter the array and overwrite the mentioned property's value with an array (output of the arrow function) based on the length check.

overwriteFieldWith<T1, T2 extends T1[keyof T1]>(
      input: Array<T1>, property: keyof T1, onOverwrite: (i: T1) => T2
): T[] {
   // filter the input array and replace the property for
   // each item with the output of the supplied arrow function
   return input.filter(i => {
       const value = onOverwrite(i);
       if(value.length) {  // giving me error 'length does not exist on type T2'
          i[property] = value;
          console.log(i);
          return true;
       } else {
          return false;
       }
   });
}

It gives me error when I try to do length check on it. The supplied arrow function is always going to return an array but how can I satisfy the compiler on this?

Edit: Link to the ts playground.

Here's one approach:

overwriteFieldWith<T, K extends keyof T>(
    input: Array<T>,
    property: K,
    onOverwrite: (i: T) => Extract<T[K], readonly any[]>
): T[] {
    return input.filter(i => {
        const value = onOverwrite(i);
        if (value.length) {
            i[property] = value;
            return true;
        } else {
            return false;
        }
    });
}

I made the function generic in the type K of property , so that the type checker keeps track of which property we're talking about. That way we can require that onOverWrite() actually return something assignable to that property value instead of something else... we need to know this or else i[property] = value might be unsafe.

I also made the return type of onOverwrite() just Extract<T[K], readonly any[]> using the Extract<T, U> union filtering utility type instead of some generic U extends T[K] .

(Note that the readonly any[] type is wider than just any[] ; we don't care about modifying the array so we don't need to require mutable methods to exist).

This serves the purpose of convincing the compiler that value.length will exist (since the compiler accepts that Extract<T, U> is assignable to both T and U , so the compiler knows that value is an readonly any[] ). It also serves the purpose of requiring that onOverwrite() return only those union members of the property type T[K] that are arrays. This works for your example use case at any rate.


Let's test it:

const filtered = a.overwriteFieldWith(elementTypes, 'elements', (i) =>
    i?.elements?.filter(data => !data.hide) ?? []);
/* (method) A.overwriteFieldWith<ElementType, "elements">(input: ElementType[], 
   property: "elements", onOverwrite: (i: ElementType) => TElement[]
): ElementType[] */

Looks good. Note that the compiler inferred T as ElementType and K as "elements" , and thus that the return type of onOverwrite() is TElment[] as demonstrated here:

type TK = ElementType['elements'];
//type TK = TElement[] | undefined

type XTK = Extract<ElementType['elements'], readonly any[]>
// type XTK = TElement[]

So an ElementType might have an undefined elements property, but onOverwrite cannot return undefined .

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