2

I want to make a TypeScript class that is loosely MyKOArray<T> extends KnockoutObservableArray<T>. Is this even possible? If so, is there a compact way to do it?

  • I read Knockout's extenders section, but it doesn't seem applicable as I'm trying to (among other things) add a state attribute -- not affect the existing subscribable.
  • I could list the things I've tried and the ways they've gone wrong (e.g. TS2507, TS2322, etc.) but I doubt they'd be informative. As a baseline, I believe some of the essential details are:
    • KnockoutObservableArray is an interface so I really need to "extend" ko.observableArray (the "factory" function).
    • The signature of ko.observableArray is not right (i.e. no new and it handles the initial value in the function rather than a constructor method).
    • I could return ko.observableArray(initial) from a constructor, but have not been able to get the prototype chain right (e.g. methods not available).
    • I could override @@create in JS to return ko.observableArray(initial), but I've had trouble finding a JS example (let alone a valid TS implementation).
  • I also found an extremely old discussion on a similar topic. The conclusion there was that it was impossible due to a lack of prototyping in KO, but the current source code (e.g. ko.utils.setPrototypeOfOrExtend) suggests that this conclusion may be out-of-date.
  • I've also dug through plenty of generic TS extends questions (e.g. Can you extend a function in TypeScript?) without finding something that seemed to match.

===

EDIT: Why?

I'm attempting to decouple my system using the following pattern (only documenting the "interesting case"):

  • ViewModel makes a request to a data service (indirectly via a factory method on a class). The data service returns an instance immediately (MyClass or KnockoutObservableArray<MyClass> for a Singleton or Collection respectively).
  • Asynchronously, the service loads/populates (and manages real-time updates to) the returned instance. This requires a subscription to a WebSocket for events and REST call(s) to load data (incl. the potential for retries).
  • The service passes its progress to the instance by calling methods on the instance (the "callback" and "errback" in my pattern).
  • These methods update a state attribute (i.e. observable) and may update something else (e.g. populate its values or populate an error message observable).
  • The UI interrogates the state observable to communicate the state of loading (and other activities) to the user.

This is working great on my single instances. The state method is interrogated (and updated) widely to ensure valid actions.

I'd like to duplicate the pattern on my KOArrays. That way, I can discriminate between an empty array that is not-yet-loaded and a loaded array that happens to be empty. I can also add a state and error_message to the KOArray to mirror the behavior of my instances.

1
  • 1
    Could you please add a short description about why exactly do you need this? It doesn't sound like a reasonable pattern in most of the cases. Commented Jan 30, 2018 at 20:53

2 Answers 2

2

In short, it's not possible the way you described.

Longer story

Knockout is not implemented in an object-oriented way. So basically almost everything it exposes are functions or plain objects, etc. The prototype handling is used there to extend the function instance which is returned by for example ko.observable. So basically inheriting from any kinds of Knockout stuff is not possible in an OOP sense.

If you need the inheritance to add functionality, you can add your new methods to ko.observableArray.fn object. Then this function will be callable on all observableArray instances afterwards. A quick example is here.

ko.observableArray.fn.hasElements = function() {
  return this().length > 0;
}
...
if (myModel.selectedItems.hasElements()) {
  ...
}

If you need the inheritance to restrict somethings, you can add your own factory function like this for example.

ko.myRestrictedObservableArray = function(initialValue) {
  if (!initialValue || !initialValue.length) {
    throw new "You can create an observable array only with a non-empty initial value";
  }

  return ko.observableArray(initialValue);
}

You can even override the original function if you wish so for example like this. Although, this is not a good practice in most cases.

var _original = ko.observableArray;
ko.observableArray = function(initialValue) {
  // ... do your custom things
  var array = _original.call(this, initialValue);
  // ... do your custom things 
  return array;
}

If you have yet another requirement, please describe it in more detail in you question.

Sign up to request clarification or add additional context in comments.

1 Comment

In case you don't get an update, I edited the question to add an example.
0

Based on @Zoltan's feedback, I ended up going with the following. I'm sure there are bugs and opportunities to optimize, but it let me write everything in a Typescript class (with appropriate type checking in both directions):

In pure JS (e.g. a <script> in the HTML file or separate JS file):

function RestCollection(initialValue) {
    const fn = ko.observableArray(initialValue);
    // Static properties
    ko.utils.extend(fn.constructor, RestCollectionMixin);
    // Methods (already constructed so must attach directly, not to prototype)
    const methods = Object.getOwnPropertyNames(RestCollectionMixin.prototype);
    for (let i=0; i<methods.length; i++) {
        if (methods[i] == "constructor") continue;  // don't move over the constructor
        fn[methods[i]] = RestCollectionMixin.prototype[methods[i]].bind(fn)
    }
    // Constructed properties
    ko.utils.extend(fn, new RestCollectionMixin());
    return fn;
} {}

// RestCollectionMixin needs this; don't return or it will break the constructor
function KnockoutObservableArrayClass() {}

In a TS file:

// abstract is not passed to JS so this prevents misuse without breaking the RestCollection constructor
abstract class RestCollectionMixin<T> extends KnockoutObservableArrayClass<T> { // extends handles parent method access
    static FSM = class {
        static STATE_INIT = 'initializing';
        static STATE_READY = 'ready';
        static STATE_SAVING = 'save_requested';
        static STATE_DELETING = 'delete_requested';
        static STATE_DELETED = 'deleted';
        static STATE_ERROR = 'error';
    };
    public states() { // convenience accessor
        return this.constructor.FSM
    }
    public state:KnockoutObservable<string> = ko.observable(this.states().STATE_INIT);

    public processRestError(results) {
        this.state(this.states().STATE_ERROR)
        ...
    }
    ...
}

Finally, a definition file for RestCollection and KnockoutObservableArrayClass so TS checking works:

// provide KnockoutObservableArray properties and methods to the Mixin
interface KnockoutObservableArrayClass<T> extends KnockoutObservableArray<T> {
    new<T>(initialValue): KnockoutObservableArray<T>
}
declare const KnockoutObservableArrayClass:KnockoutObservableArrayClass<any>;

// Define the focal class, capturing all extensions in RestCollectionMixin
interface RestCollection<T> extends RestCollectionMixin<T> {
    new<T>(initialValue): RestCollection<T>
}
declare const RestCollection:RestCollection<any>;

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.