mlangwell mlangwell - 24 days ago 9
TypeScript Question

filter is not creating a new array?

I have a problem with the following code, and maybe I am just not understanding how filter works. From my understanding filter is supposed to return a new array. The filteredItems is what I am doing the *ngFor on in the DOM. So I first do a shallow copy of my input items array with the slice method into my filteredItems and copyItems. After that I attempt to return a new array of filtered items from the shallow copied items array. However, whenever I try to filter the array of items it actually manipulates the original array's data instead of just returning a new array with the data I need.

@Input() private items: Items[];

private copyItems = new Array<Items>();

public input = new FormControl();
public filteredItems = new Array<Items>();

ngOnInit() {
this.filteredItems = this.items.slice();
this.copyItems = this.items.slice();

this.subscription = this.input.valueChanges
.debounceTime(500)
.distinctUntilChanged()
.map((value) => {
return value;
})
.subscribe((searchTerm) => {
if (searchTerm) {
this.filteredItems = this.filterItems(searchTerm.toLowerCase());
console.log(this.items);
} else {
this.copyItems = this.items.slice();
this.filteredItems = this.items.slice();
}
});
}

private filterItems(searchTerm: string): Array<Items> {
return this.copyItems.filter((item) => {
let filterExists: boolean = false;
let itemName = <string>item.name;
if (itemName.toLowerCase().startsWith(searchTerm)) {
filterExists = true;
}

let filteredChildren = this.filterChildren(searchTerm, item);

if (filteredChildren.length > 0) {
filterExists = true;
item.children = filteredChildren;
}

if (filterExists)
return true;
else
return false;
});
}

private filterChildren(searchTerm: string, item: Items): Array<ChildItems> {
return item.children.filter(child => {
let childName = <string>child.name;
if (childName.toLowerCase().startsWith(searchTerm)) {
return child;
}
});
}


Can someone please tell me what the heck I am doing wrong here. I have been banging my head against my desk reworking this problem over and over for the past two days and cannot figure it out.

Thanks in advance!

Answer Source

filter does indeed create a new array, but the objects referenced in the original are also referenced by the new array, so changes to them are visible through both.

If you're going to change the state of an item in the array and don't want that change visible through the old array, you need to create a new item and use that in the array. That would be map rather than filter — or rather, in your case, a combination of the two.

Here's a simpler example of what you're currently doing; note how state becomes 2 regardless of which array we look it it in:

var original = [
  {id: 1, state: 1},
  {id: 2, state: 1},
  {id: 3, state: 1}
];
var filtered = original.filter(item => {
  if (item.id % 2 == 1) {
    ++item.state;
    return item;
  }
});
console.log("original", original);
console.log("filtered", filtered);
.as-console-wrapper {
  max-height: 100% !important;
}

Sticking to existing Array.prototype functions, the way you'd do a mutating filter would be to use map then filter removing undefined entries:

var original = [
  {id: 1, state: 1},
  {id: 2, state: 1},
  {id: 3, state: 1}
];
var filtered = original
  .map(item => {
    if (item.id % 2 == 1) {
      return {id: item.id, state: item.state + 1};
    }
  })
  .filter(item => !!item);
console.log("original", original);
console.log("filtered", filtered);
.as-console-wrapper {
  max-height: 100% !important;
}

Alternately, you could give yourself a mapFilter function:

Object.defineProperty(Array.prototype, "mapFilter", {
  value: function(callback, thisArg) {
    var rv = [];
    this.forEach(function(value, index, arr) {
      var newValue = callback.call(thisArg, value, index, arr);
      if (newValue) {
        rv.push(newValue);
      }
    })
    return rv;
  }
});
var original = [
  {id: 1, state: 1},
  {id: 2, state: 1},
  {id: 3, state: 1}
];
var filtered = original
  .mapFilter(item => {
    if (item.id % 2 == 1) {
      return {id: item.id, state: item.state + 1};
    }
  });
console.log("original", original);
console.log("filtered", filtered);
.as-console-wrapper {
  max-height: 100% !important;
}

...but all the caveats about extending built-in prototypes apply (you might make it a utility you pass the array to instead).