0

I've written a construct similar to the following in an Angular app (this has been greatly simplified to demonstrate the issue). What would prevent the filterProperty() function from being defined on the item instance inside of the DemoSource class?

export keyword is used because each construct is defined in a separate file.

export interface IProperty {
    filterProperty(): string;
}

export class Demo implements IProperty {
    displayName: string;

    filterProperty(): string {
        return this.displayName;
    }
}

export class DemoSource<TItem extends IProperty> {
    filterChange = new BehaviorSubject('');
    filteredData: TItem[];

    constructor(private service: IService<TItem>) {
        // A BehaviorSubject<Array<TItem>> from a service
        this.filteredData = service.data.value.slice();
    }

    connect(): Observable<TItem[]> {
        return Observable.merge(this.service.data).map(() => {
            this.filteredData = this.service.data.value.slice().filter((item: TItem) => {
                // Object doesn't support property or method 'filterProperty'
                const searchStr = item.filterProperty().toLowerCase();
                return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
            });

            return filteredData;
        });
    }
}

When debugging at the point where item.filter() is called, I get the following error:

ERROR TypeError: Object doesn't support property or method 'filterProperty'

Update
Changed the IProperty contract function from filter() to filterProperty() to avoid confusion.

Here is the Error:

error

Here, you can see how the item instance has all of its properties properly populated, but has no filterProperty() function defined (it's not in proto either):

item

Update
Here are the service details:

@Injectable()
export class IdentityService implements IService<AppUser> {
    users = new BehaviorSubject<Array<AppUser>>([]);
    public get data(): BehaviorSubject<AppUser[]> { return this.users; }
}

export interface IService<T> {
    data: BehaviorSubject<T>;
}

Here is the service being populated with data from the API:
service

Here is the result of a pure API call from the browser:
api

Certain properties have been redacted because of their data

Update - Transpiled JavaScript

Object.defineProperty(exports, "__esModule", { value: true });
var Demo = (function () {
    function Demo() {}
    Object.defineProperty(Demo.prototype, "filter", {
        get: function () { return this.displayName; },
        enumerable: true,
        configurable: true
    });
    return Demo;
}());
exports Demo = Demo;

Update
Web App demonstrating the issue: Typescript / Web API Interface Issue
GitHub Repo for the Web App: typescript-interface-issues

18
  • What is the content of this.service.data.value at runtime? What is the type of item at runtime? Commented Aug 23, 2017 at 15:45
  • this.service.data.value is an Array<Demo> at runtime (in this given scenario), and only has the display property available and properly populated. Commented Aug 23, 2017 at 15:47
  • Where exactly does this.service.data.value come from? The fact that you've typed it as Demo Interface doesn't add filter method. And error message proves it. item object is supposed to have filter method, and it doesn't have it. Commented Aug 23, 2017 at 15:59
  • service.data.value corresponds to a BehaviorSubject<Array<Demo>> in an Angular service that is populated as the result of a Web API call. Commented Aug 23, 2017 at 16:00
  • It doesn't correspond, hence the error. item should be an instance of Demo class in order to get filter method, and it's not. Commented Aug 23, 2017 at 16:09

3 Answers 3

3

Data results from JSON response and is a structure of plain objects and arrays that only have methods that are defined on their prototypes, Object and Array.

item isn't really a class instance and doesn't have filterProperty method it's supposed to have. So it's incorrect to specify DemoSource<IProperty>, considering that IProperty is supposed to have filterProperty. This fools TypeScript into thinking that objects have this method, while they don't have it - they are still plain objects, specified types don't change them.

An interface that is used for generic is supposed to reflect data structure properties (not methods). For classes that are supposed to be constructed from plain objects it's a good practice to accept plain object in constructor:

export interface IItemData {
    displayName: string;
    id?: number;
    ...
}

export class Item implements IItemData {
    displayName: string;

    constructor({ displayName }: IItemData) {
        this.displayName = displayName;
    }

    filterProperty(): string {
        return this.displayName;
    }
}

Then data structure should be processed and plain items should be converted to Item instances:

export class DemoSource<TItem extends IItemData> {
    ...
    this.filteredData = this.service.data.value.slice()
    .map((item: TItem) => {
        // item doesn't have 'filterProperty'
        return new Item(item); 
    })
    .filter((item: Item) => {
        // item has 'filterProperty'
        const searchStr = item.filterProperty().toLowerCase();
        return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
    });
    ...
Sign up to request clarification or add additional context in comments.

18 Comments

I just found out today in another comment thread that apparently the new Angular 4 HttpClient can do all of this at the time of receiving the data (more or less). You can provide a type definition to the get function, and it will type it before sending you the result. Haven't been table to test it out yet, though. http.get<TypeHere>('/apiurl').subscribe( ....
@diopside It will be same case as code above and will have same problems when done incorrectly. Types don't affect output code. http.get<IItemData[]> assumes that received data has specified data structure and will likely be the right thing to do. http.get<Item[]> assumes that data consists of Item instances and is wrong about that, because it's not.
oh right - bc hes specifying a class not an interface. I have been staring at the computer too long, I thought he was trying to define the first two exports in his file as interfaces and the third as a class.
Technically, IProperty in the OP is an interface, but it's same thing because an interface contains a method and the result is supposed to be class instance, while it's just plain object.
If I have to directly instantiate item to an instance of type Item, doesn't that defeat the purpose of using Generics / Interfaces in the first place? The intent here is to be able to use the DemoSource class with any item that extends the IProperty so that you can indirectly set the property to be filtered for any given class.
|
0

As I think filter is reserved keyword. So that angular trying to implement filter function on type Object. filter can be used only for Array . try using different name for the function.

If this is not the case. Maybe the service data item should come as Object instead of TTem instance.We can get the class methods only on its instances. Can you try creating new instance as follows

this.filteredData = this.service.data.value.slice().filter(
(item: TItem) => {
            // Object doesn't support property or method 'filter'
            let instance: TItem = new TItem();
            Object.assign(instance, item);
            const searchStr = instance.filter().toLowerCase();
            return searchStr.indexOf(this.filter.toLowerCase()) !==-1;
        });

4 Comments

As mentioned in a comment on the below answer, changing the function name had no effect.
I have modified the answer. Try to check is this can work..!!
Getting: ERROR TypeError: Object.assign: argument is not an Object
Object.assign() is a buit in function . How i may not worked !!. Check similar builtin fuctions are working properly for you i.e Object.keys()
0

I was able to get it working the way I had originally wanted it to with the below setup. The key was, apparently, adding a filter property to the C# object that was being used to serialize data to JSON via Web API. Not sure why this would be required, as TypeScript should be able to extend the model received from Web API with additional functionality.

Example simplified to demonstrate the problem set

DemoModel.cs

// C# Class JSON is serialized from via Web API
public class DemoModel
{
    public string displayName { get; set; }
    public string filter
    {
        get { return displayName; }
    }
}

iproperty.interface.ts

export interface IProperty {
    filter: string;
}

demo.model.ts

import { IProperty } from '../interfaces/iproperty.interface';

export class Demo implements IProperty {
    displayName: string;
    get filter(): string { return this.displayName; }
}

core.datasource.ts

export class CoreDataSource<TItem extends IProperty> {
    filterChange = new BehaviorSubject('');
    filteredData: TItem[];
    get filter(): string { return this.filterChange.value; }
    set filter(filter: string) { this.filterChange.next(filter); }

    constructor(private service: IService<TItem>) {
        super();
        this.filteredData = service.data.value.slice();
    }

    connect(): Observable<TItem[]> {
        const displayDataChanges = [
            this.service.data,
            this.filterChange
        ];

        return Observable.merge(...displayDataChanges).map(() => {
            this.filteredData = this.service.data.value.slice().filter((item: TItem) => {
                const searchStr = item.filter.toLowerCase();
                return searchStr.indexOf(this.filter.toLowerCase()) !== -1;
            });

            return filteredData;
        });
    }
}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.