Home > OS >  How to type a function that can take and return arrays of strings or arrays of objects?
How to type a function that can take and return arrays of strings or arrays of objects?

Time:11-15

I have a directive that in Input can get array of string or array of objects. The directive is to filter the list from @Input() and emit a new list in @Output(). Inside the directive, I created a method that does all the magic, but I can't correctly type @Input() and the function itself.

export class SelectFilteringDirective<T> {
  @Input() readonly optionsList: T[] = [];
  @Output() filteredList = new EventEmitter<T[]>();

  constructor() {}

  @HostListener('keyup', ['$event'])
  onKeyup(event: Event): void {
    this.filteredList.emit(
      this.filterList(
        this.optionsList,
        (event.target as HTMLInputElement).value
      )
    );
  }

  private filterList(data: T[], value: string): any {
    return typeof data[0] === 'object'
      ? [...data].filter(
          (item) => item.value.toLowerCase().indexOf(value.toLowerCase()) != -1
        )
      : [...data].filter(
          (item) => item.toLowerCase().indexOf(value.toLowerCase()) != -1
        );
  }
}

In the variant with the object I get the error:

Property 'value' does not exist on type 'T'.

However, in the variant with array of strings, I get the error:

Property 'toLowerCase' does not exist on type 'T'.

CodePudding user response:

If I got your question correctly, you have a directive that receives one @Input which can be either an Array of strings or an Array of objects. Based on that, here's a solution for this:

type KeyValue = Readonly<{
  key: string;
  value: string;
}>;
type ListItem = string | KeyValue;

@Directive({ selector: 'selectFiltering' })
export class SelectFilteringDirective {
  @Input() optionsList: readonly ListItem[] = [];
  @Output() readonly filteredList = new EventEmitter<ListItem[]>();

  @HostListener('keyup', ['$event'])
  onKeyup(event: Event): void {
    this.filteredList.emit(
      this.filterList(
        this.optionsList,
        (event.target as HTMLInputElement).value,
      )
    );
  }

  private filterList(
    list: readonly ListItem[],
    value: string
  ): ListItem[] {
    const lowerCasedValue = value.toLowerCase();
    return list.filter((item) =>
      (typeof item === 'string' ? item : item.value)
        .toLowerCase()
        .includes(lowerCasedValue)
    );
  }
}

Tip:

While this might solve the problem, I'm not too sure @Directive would be the most correct way to do this. Again, if I got your question correctly, you have some options and you want to filter them out based on input changes... if that's the case, I'd suggest to use a @Pipe (pure, ofc) instead.

Just in case you want to see the @Pipe version, here you go:

DEMO using @Pipe

CodePudding user response:

You can define the input type like so:

@Input() variantOne: string[];
@Input() variantTwo: {key: string, value: string}[]; // although an interface would be better

CodePudding user response:

You don't need any generics.

Change your code to this:

interface Item {
    readonly value: string;
}

type StringsOrObjectsArray = readonly String[] | readonly Item[];

function isItem( x: unknown ): x is Item {
    const whatIf = x as Item;
    return (
        typeof whatIf === 'object' &&
        whatIf !== null &&
        typeof whatIf.value === 'string'
    );
}

//

export class SelectFilteringDirective {

  @Input()  optionsList: StringsOrObjectsArray = [];
  @Output() filteredList                       = new EventEmitter<StringsOrObjectsArray>();

  constructor() {
  }

  @HostListener('keyup', ['$event'])
  onKeyup(event: KeyboardEvent): void {

    const inputValue = (event.target as HTMLInputElement).value;
    const oldList    = this.optionsList;
    const newList    = this.filterList( oldList, inputValue );

    this.filteredList.emit( newList );
  }

  private filterList(data: StringsOrObjectsArray, value: string): StringsOrObjectsArray {

    // Preconditions: (defensive-programming is essential in JS and TS!)
    if( !Array.isArray( data ) ) throw new Error( "Expected an array." );
    if( data.length < 1 ) throw new Error( "Expected a non-empty array." );

    const valuePattern = new RegExp( escapeRegExp( value ), 'i' );

    if( typeof data[0] === 'string' ) {

        return data.filter( str => valuePattern.test( str ) );
    }
    else if( isItem( data[0] ) ) {
        // The `isItem` type-guard is unnecessary as TypeScript *should* infer that `data` is `readonly Item[]` here.
        return data.filter( obj => valuePattern.test( obj.value ) );
    }
    else {
        throw new Error( "Unexpected array contents." );
    }
}


function escapeRegExp(str: string): string {
    // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
  return str.replace(/[.* ?^${}()|[\]\\]/g, '\\$&');
}
  • Related