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:
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, '\\$&');
}