Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Types: Add types for ReadonlyObservable, ReaonlyObservableArray, ReadonlyComputed #2476

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
73 changes: 54 additions & 19 deletions build/types/knockout.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,24 @@ export interface Subscription {

type Flatten<T> = T extends Array<infer U> ? U : T;

export interface SubscribableFunctions<T = any> extends Function {
init<S extends Subscribable<any>>(instance: S): void;

notifySubscribers(valueToWrite?: T, event?: string): void;

export interface ReadonlySubscribableFunctions<T = any> extends Function {
subscribe<TTarget = void>(callback: SubscriptionCallback<utils.ArrayChanges<Flatten<T>>, TTarget>, callbackTarget: TTarget, event: "arrayChange"): Subscription;

subscribe<TTarget = void>(callback: SubscriptionCallback<T, TTarget>, callbackTarget: TTarget, event: "beforeChange" | "spectate" | "awake"): Subscription;
subscribe<TTarget = void>(callback: SubscriptionCallback<undefined, TTarget>, callbackTarget: TTarget, event: "asleep"): Subscription;
subscribe<TTarget = void>(callback: SubscriptionCallback<T, TTarget>, callbackTarget?: TTarget, event?: "change"): Subscription;
subscribe<X = any, TTarget = void>(callback: SubscriptionCallback<X, TTarget>, callbackTarget: TTarget, event: string): Subscription;

getSubscriptionsCount(event?: string): number;
}

export interface SubscribableFunctions<T = any> extends ReadonlySubscribableFunctions<T> {
init<S extends Subscribable<any>>(instance: S): void;

notifySubscribers(valueToWrite?: T, event?: string): void;

extend(requestedExtenders: ObservableExtenderOptions): this;
extend<S extends Subscribable<any>>(requestedExtenders: ObservableExtenderOptions): S;

getSubscriptionsCount(event?: string): number;
}

export interface Subscribable<T = any> extends SubscribableFunctions<T> { }
Expand All @@ -49,15 +51,32 @@ export function isSubscribable<T = any>(instance: any): instance is Subscribable

export type MaybeObservable<T = any> = T | Observable<T>;

export interface ObservableFunctions<T = any> extends Subscribable<T> {
equalityComparer(a: T, b: T): boolean;
export interface ReadonlyObservableFunctions<T = any> extends ReadonlySubscribableFunctions<T> {
peek(): T;
}

export interface ObservableFunctions<T = any> extends ReadonlyObservableFunctions<T>, SubscribableFunctions<T> {
equalityComparer(a: T, b: T): boolean;
valueHasMutated(): void;
valueWillMutate(): void;
}

export interface Observable<T = any> extends ObservableFunctions<T> {
/**
* The part of an observable contract that do not mutate the underlying value - while most observables are writable at runtime
* it can be useful to cast values to this type, just like it can be useful to cast writable arrays
* to the native TS ReadonlyArray type.
*
* Computeds can also be cast to this type.
*
* NOTE: does not support .extend:
* Although some extenders are safe (ones which create a new observable or computed)
* others are not (e.g. deferred, notify) and modify the underlying observable
* */
export interface ReadonlyObservable<T = any> extends ReadonlyObservableFunctions<T> {
(): T;
}

export interface Observable<T = any> extends ObservableFunctions<T>, ReadonlyObservable<T> {
(value: T): any;
}

Expand All @@ -78,7 +97,10 @@ export function isWritableObservable<T = any>(instance: any): instance is Observ

export type MaybeObservableArray<T = any> = T[] | ObservableArray<T>;

export interface ObservableArrayFunctions<T = any> extends ObservableFunctions<T[]> {
/**
* The part of an observable array contract that do not mutate the underlying value - see ReadableObservable type for rationale
*/
export interface ReadonlyObservableArrayFunctions<T = any> extends ReadonlyObservableFunctions<T[]> {
// General Array functions
indexOf(searchElement: T, fromIndex?: number): number;

Expand All @@ -88,6 +110,14 @@ export interface ObservableArrayFunctions<T = any> extends ObservableFunctions<T
splice(start: number): T[];
splice(start: number, deleteCount: number, ...items: T[]): T[];

// Ko specific
reversed(): T[];

sorted(compareFunction?: (left: T, right: T) => number): T[];
}

export interface ObservableArrayFunctions<T = any> extends ReadonlyObservableArrayFunctions<T>, ObservableFunctions<T[]> {
// General Array functions
pop(): T;
push(...items: T[]): number;

Expand All @@ -99,10 +129,6 @@ export interface ObservableArrayFunctions<T = any> extends ObservableFunctions<T
sort(compareFunction?: (left: T, right: T) => number): this;

// Ko specific
reversed(): T[];

sorted(compareFunction?: (left: T, right: T) => number): T[];

replace(oldItem: T, newItem: T): void;

remove(item: T): T[];
Expand All @@ -118,7 +144,9 @@ export interface ObservableArrayFunctions<T = any> extends ObservableFunctions<T
destroyAll(items: T[]): void;
}

export interface ObservableArray<T = any> extends Observable<T[]>, ObservableArrayFunctions<T> {
export interface ReadonlyObservableArray<T = any> extends ReadonlyObservable<T[]>, ReadonlyObservableArrayFunctions<T> {}

export interface ObservableArray<T = any> extends Observable<T[]>, ReadonlyObservableArray<T>, ObservableArrayFunctions<T> {
(value: T[] | null | undefined): this;
}

Expand All @@ -138,19 +166,26 @@ export type ComputedReadFunction<T = any, TTarget = void> = Subscribable<T> | Ob
export type ComputedWriteFunction<T = any, TTarget = void> = (this: TTarget, val: T) => void;
export type MaybeComputed<T = any> = T | Computed<T>;

export interface ComputedFunctions<T = any> extends Subscribable<T> {
export interface ReadonlyComputedFunctions<T = any> extends Subscribable<T> {
dispose(): void;
}
export interface ComputedFunctions<T = any> extends ReadonlyComputedFunctions<T> {
// It's possible for a to be undefined, since the equalityComparer is run on the initial
// computation with undefined as the first argument. This is user-relevant for deferred computeds.
equalityComparer(a: T | undefined, b: T): boolean;
peek(): T;
dispose(): void;
isActive(): boolean;
getDependenciesCount(): number;
getDependencies(): Subscribable[];
}

export interface Computed<T = any> extends ComputedFunctions<T> {
/**
* The part of an computed contract that do not mutate the underlying value - see ReadableObservable type for rationale
*/
export interface ReadonlyComputed<T = any> extends ReadonlyComputedFunctions<T> {
(): T;
}
export interface Computed<T = any> extends ComputedFunctions<T>, ReadonlyComputed<T> {
(value: T): this;
}

Expand Down
47 changes: 47 additions & 0 deletions spec/types/module/test-readonly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { ReadonlyObservable, Observable, ReadonlyComputed} from "knockout";
import * as ko from "knockout";

function testReadonlyObservable() {
const write = ko.observable("foo");
write("bar");
const read = write as ReadonlyObservable<string>;

read(); // $ExpectType string
read.subscribe(() => {}); // Can still subscribe
// But can't write to it
// read("foo") // $ExpectError // Don't currently have a good mechanism for testing this outside of DT repo
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is pretty much copied from the DefinitelyTyped repo; unfortunately it's pretty hard to write a test that ensures a particular expression doesn't compile in TS. They have some fairly sophisticated test mechanisms that allowed invalid lines to be annotated with this //$ExpectError comment, and the test would fail if the line didn't error.

I couldn't find a good way to replicate that here, generally, so I just left the comments for posterity.


const writeAgain = read as Observable<string>
writeAgain("bar");
};

function testReadonlyObservableArray() {
// Normal observable array behavior
const write = ko.observableArray(["foo"]);
write(["bar"]);
write.push("foo");

// Readonly observable array
const read = write as ko.ReadonlyObservableArray<string>;
read(); //$ExpectType ReadonlyArray<string>
read.slice(0, 1); //$ExpectType string[]

// read(["foo"]); // $ExpectError // no way to test this, currently
const _hasPushMethod: typeof read extends { push: any } ? true : false = false;

// Can cast back to a writeable
const writeAgain = read as ko.ObservableArray<string>
writeAgain(["foo"]);
}

function testReadonlyComputed() {
const write = ko.computed({
read: () => {},
write: () => {},
});

// Can cast a computed as readonly
const read: ReadonlyComputed<any> = write;
read();
// read("foo"); // $ExpectError // no way to test this currently
}