How can I inherit from a knockout class using Typescript?

1.9k views Asked by At

I'm using knockout 3.x with Typescript and I want to make a strongly typed collection class that inherits from KnockoutObservableArray<T>, so that my type can BE an observable array but also have its own methods. So I tried this:

export class Emails implements KnockoutObservableArray<Email>
{
    //add a new email to the list
    addItem() : void
    {
        var email = new ContactEmail();
        ...
        this.push(email);
    };
}

But this gives me the following error:

Class Emails declares interface KnockoutObservableArray<Email> but does not implement it:
Type 'Emails' is missing property 'extend' from type 'KnockoutObservableArray<Email>'.

If I try to add the extend method, then it requires that I implement the peek method, and so on, suggesting that I need to implement all of KnockoutObservableArray. But I don't want to reimplement it, I want to extend it, and since KnockoutObservableArray is an interface and not a class, I don't see any way to do that. Is there a way?

4

There are 4 answers

8
Fenton On

The implements keyword is used to state that you will implement the supplied interface... to satisfy this condition, you'll need to implement all of the methods defined in the following interfaces (because you need to follow the whole chain from KnockoutObservableArray<T>.

Alternatively, create a smaller interface to represent what you actually need from your custom class, and if it is a sub-set of all of this, you will be able to use your interface for your custom class as well as for normal KnockoutObservableArray<T> classes (which will pass the interface test structurally).

interface KnockoutObservableArray<T> extends KnockoutObservable<T[]>, KnockoutObservableArrayFunctions<T> {
    extend(requestedExtenders: { [key: string]: any; }): KnockoutObservableArray<T>;
}

interface KnockoutObservable<T> extends KnockoutSubscribable<T>, KnockoutObservableFunctions<T> {
    (): T;
    (value: T): void;

    peek(): T;
    valueHasMutated?:{(): void;};
    valueWillMutate?:{(): void;};
    extend(requestedExtenders: { [key: string]: any; }): KnockoutObservable<T>;
}

interface KnockoutObservableArrayFunctions<T> {
    // General Array functions
    indexOf(searchElement: T, fromIndex?: number): number;
    slice(start: number, end?: number): T[];
    splice(start: number): T[];
    splice(start: number, deleteCount: number, ...items: T[]): T[];
    pop(): T;
    push(...items: T[]): void;
    shift(): T;
    unshift(...items: T[]): number;
    reverse(): T[];
    sort(): void;
    sort(compareFunction: (left: T, right: T) => number): void;

    // Ko specific
    replace(oldItem: T, newItem: T): void;

    remove(item: T): T[];
    remove(removeFunction: (item: T) => boolean): T[];
    removeAll(items: T[]): T[];
    removeAll(): T[];

    destroy(item: T): void;
    destroy(destroyFunction: (item: T) => boolean): void;
    destroyAll(items: T[]): void;
    destroyAll(): void;
}

interface KnockoutSubscribable<T> extends KnockoutSubscribableFunctions<T> {
    subscribe(callback: (newValue: T) => void, target?: any, event?: string): KnockoutSubscription;
    subscribe<TEvent>(callback: (newValue: TEvent) => void, target: any, event: string): KnockoutSubscription;
    extend(requestedExtenders: { [key: string]: any; }): KnockoutSubscribable<T>;
    getSubscriptionsCount(): number;
}

interface KnockoutSubscribableFunctions<T> {
    notifySubscribers(valueToWrite?: T, event?: string): void;
}

interface KnockoutObservableFunctions<T> {
    equalityComparer(a: any, b: any): boolean;
}

Here is an example of the alternative... assuming you need push and remove as an example. (Side note: inheritance isn't possible here as these are all interfaces - if Knockout supports inheritance, the type definition could be changed to allow the work-free extends rather than the hard work implements version of this answer...)

The interesting bit in this example is the first interface - it is a custom one you define, but anything that matches it will do, including a KnockoutObservableArray<Email>. This is an example of structural typing saving you some time. Structural typing wants you to go to the pub.

/// <reference path="scripts/typings/knockout/knockout.d.ts" />

interface MyObservableArray<T> {
    push(...items: T[]): void;
    remove(item: T): T[];
}

class Email {
    user: string;
    domain: string;
}

class ObservableEmailArray implements MyObservableArray<Email> {
    push(...items: Email[]): void {

    }

    remove(item: Email): Email[] {
        return [];
    }
}

var observableArrayOne: KnockoutObservableArray<Email> = ko.observableArray<Email>();
var observableArrayTwo: MyObservableArray<Email> = new ObservableEmailArray();

function example(input: MyObservableArray<Email>) {
    // Just an example - will accept input of type MyObservableArray<Email>...
}

example(observableArrayOne);

example(observableArrayTwo);
2
shawty On

In the not too distant past, this was something that used to plague me too.

Why?

Quite simply, for 2 reasons.

The first reason was I didn't really understand JavaScript from an object point of view. To me it had always been a "Scripting Language" and that was it, I'd heard people tell me otherwise, but I was (still am) a C# person, so I largely ignored what was said because it didn't look like C# objects.

All that changed, when I started to use Type Script, and TS showed me in the form of it's JS output what object JS looked like.

The second reason was the very flexibility of JS and more importantly knockout.

I loved the idea of simply just changing my JSON endpoint in C# code, and not really having to make any changes in the front end other than a few tweaks to my HTML.

If I wanted to add a new field to an output object, then I could, followed by simply adding a new column to a table, binding the correct field name and job done.

Fantastic right.

Indeed it was, until I started getting asked to provide row based functionality. It started simple with requests like being able to delete rows and do things like inline editing.

That lead to many strange adventures such as this:

Knockout JS + Bootstrap + Icons + html binding

and this:

Synchronizing a button class with an option control using knockoutjs

But the requests got stranger and more complex, and I started coming out with ever crazy ways of traversing the DOM, to get from the row I was on, up to the parent and back down to my sibling, then yanking the text from adjacent elements and other craziness.

Then I saw the light

I started working on a project with a junior dev who only ever knew and/or lived in JS land, and he simply asked me one question.

"If it's all causing you so much pain, then why don't you just make a view model for your rows?"

And so the following code was born.

var DirectoryEntryViewModel = (function ()
{
  function DirectoryEntryViewModel(inputRecord, parent)
  {
    this.Pkid = ko.observable(0);
    this.Name = ko.observable('');
    this.TelephoneNumber = ko.observable('');
    this.myParent = parent;

    ko.mapping.fromJS(inputRecord, {}, this);

  }

  DirectoryEntryViewModel.prototype.SomePublicFunction = function ()
  {
    // This is a function that will be available on every row in the array
    // You can call public functions in the base VM using "myParent.parentFunc(blah)"
    // you can pass this row to that function in the parent using  "myParent.someFunc(this)"
    // and the parent can simply do "this.array.pop(passedItem)" or simillar
  }

  return DirectoryEntryViewModel;
})();

var IndexViewModel = (function ()
{
  function IndexViewModel(targetElement)
  {
    this.loadComplete = ko.observable(false);
    this.phoneDirectory = ko.observableArray([]);
    this.showAlert = ko.computed(function ()
    {
      if (!this.loadComplete())
        return false;
      if (this.phoneDirectory().length < 1)
      {
        return true;
      }
      return false;
    }, this);

    this.showTable = ko.computed(function ()
    {
      if (!this.loadComplete())
        return false;
      if (this.phoneDirectory().length > 0)
      {
        return true;
      }
      return false;
    }, this);

    ko.applyBindings(this, targetElement);
    $.support.cors = true;
  }

  IndexViewModel.prototype.Load = function ()
  {
    var _this = this;
    $.getJSON("/module3/directory", function (data)
    {
      if (data.length > 0)
      {
        _this.phoneDirectory(ko.utils.arrayMap(data, function (item)
        {
          return new DirectoryEntryViewModel(item, _this);
        }));
      } else
      {
        _this.phoneDirectory([]);
      }
      _this.loadComplete(true);
    });
  };

  return IndexViewModel;
})();

window.onload = function ()
{
  var pageView = document.getElementById('directoryList');
  myIndexViewModel = new IndexViewModel(pageView);
  myIndexViewModel.Load();
};

Now this is not by any stretch of the imagination the best example (I've just yanked it out of a project I had to hand), but it works.

Yes, you have to make sure that if you add a field to your backend in the JSON, that you also add it to the row view model (Which you can load with RequireJS or similar) , as well as adding that column to your table/list/other markup where needed.

But for the sake of an extra line or two, you get to add a function once, easily, that then is available on every row in the collection.

And, just on a note of typescript, the entire JS above was generated by the TS compiler, from a TS source that implements the same pattern.

I have quite a few things running in this manner (There are a couple of examples on my github page - http://github.com/shawty you can clone), some of the apps I have running like this have entire view-models that adapt an entire UI based on a single simple change in a computed observable, I have rows that manage their own state (Including talking directly to the database) then update their state in the parent table once an operation has been successful.

Hopefully that will give you some more food for thought. While you can probably wrestle on down the road your taking, trying to extend the existing KO classes, I think in all honesty you'll find the above pattern much easier to get to grips with.

I tried the same approach as you once over, but I abandoned it fairly quickly once my JS friend pointed out how easy it was to just create a VM for the row and re-use it.

Hope it helps Shawty

0
zielu1 On

I had the same problem and found workaround.

   export class ShoppingCartVM {
        private _items: KnockoutObservableArray<ShoppingCartVM.ItemVM>;
        constructor() {
            this._items = ko.observableArray([]);
            var self = this;
            (<any>self._items).Add = function() { self.Add(); }
            return <any>self._items;
        }

        public Add() {
            this._items.push(new ShoppingCartVM.ItemVM({ Product: "Shoes", UnitPrice: 1, Quantity: 2 }));
        }
    }

You can't cast ShoppingCartVM into KnockoutObservableArray, but you can introduce common interface like Steve Fenton answered.

0
Philip Bijker On

What you're actually looking for is a class that extends KnockoutObservableArray instead of a class that implements it.

Extending however requires an implementation instead of an interface and this implementation is not available in Typescript. There's only a static interface KnockoutObservableArrayStatic which returns an implementation, exactly resembling knockout's ko.observableArray implementation.

I solved my similar case with favoring composition over inheritance. For example:

export class Emails
{
    collection = ko.observableArray<Email>();

    //add a new email to the list
    addItem() : void
    {
        var email = new ContactEmail();
        ...
        this.collection.push(email);
    };
}

You can access the collection property for binding to the DOM or if you need any functionality of the observableArray:

<div data-bind="foreach: myEmails.collection">
...
</div>