diff --git a/.gitignore b/.gitignore index a6f7c48ba..33a946907 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,19 @@ -*.suo -*.swp -*.csproj.user -bin -obj -*.pdb -_ReSharper* -*.ReSharper.user -*.ReSharper -desktop.ini -.eprj -perf/* -*.orig -*.bak -.DS_Store -npm-debug.log -node_modules -dist \ No newline at end of file +*.suo +*.swp +*.csproj.user +bin +obj +*.pdb +_ReSharper* +*.ReSharper.user +*.ReSharper +desktop.ini +.eprj +perf/* +*.orig +*.bak +.DS_Store +npm-debug.log +node_modules +dist +/.vscode diff --git a/build/types/knockout.d.ts b/build/types/knockout.d.ts index e7e4e8763..c7db6cef5 100644 --- a/build/types/knockout.d.ts +++ b/build/types/knockout.d.ts @@ -16,11 +16,7 @@ export interface Subscription { type Flatten = T extends Array ? U : T; -export interface SubscribableFunctions extends Function { - init>(instance: S): void; - - notifySubscribers(valueToWrite?: T, event?: string): void; - +export interface ReadonlySubscribableFunctions extends Function { subscribe(callback: SubscriptionCallback>, TTarget>, callbackTarget: TTarget, event: "arrayChange"): Subscription; subscribe(callback: SubscriptionCallback, callbackTarget: TTarget, event: "beforeChange" | "spectate" | "awake"): Subscription; @@ -28,10 +24,16 @@ export interface SubscribableFunctions extends Function { subscribe(callback: SubscriptionCallback, callbackTarget?: TTarget, event?: "change"): Subscription; subscribe(callback: SubscriptionCallback, callbackTarget: TTarget, event: string): Subscription; + getSubscriptionsCount(event?: string): number; +} + +export interface SubscribableFunctions extends ReadonlySubscribableFunctions { + init>(instance: S): void; + + notifySubscribers(valueToWrite?: T, event?: string): void; + extend(requestedExtenders: ObservableExtenderOptions): this; extend>(requestedExtenders: ObservableExtenderOptions): S; - - getSubscriptionsCount(event?: string): number; } export interface Subscribable extends SubscribableFunctions { } @@ -49,15 +51,32 @@ export function isSubscribable(instance: any): instance is Subscribable export type MaybeObservable = T | Observable; -export interface ObservableFunctions extends Subscribable { - equalityComparer(a: T, b: T): boolean; +export interface ReadonlyObservableFunctions extends ReadonlySubscribableFunctions { peek(): T; + equalityComparer(a: T, b: T): boolean; +} + +export interface ObservableFunctions extends ReadonlyObservableFunctions, SubscribableFunctions { valueHasMutated(): void; valueWillMutate(): void; } -export interface Observable extends ObservableFunctions { +/** + * 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 extends ReadonlyObservableFunctions { (): T; +} + +export interface Observable extends ObservableFunctions, ReadonlyObservable { (value: T): any; } export function observable(value: T): Observable; @@ -70,8 +89,8 @@ export module observable { export function isObservable(instance: any): instance is Observable; -export function isWriteableObservable(instance: any): instance is Observable; -export function isWritableObservable(instance: any): instance is Observable; +export function isWriteableObservable(instance: any): instance is Observable | WritableComputed; +export function isWritableObservable(instance: any): instance is Observable | WritableComputed; //#endregion @@ -79,7 +98,10 @@ export function isWritableObservable(instance: any): instance is Observ export type MaybeObservableArray = T[] | ObservableArray; -export interface ObservableArrayFunctions extends ObservableFunctions { +/** + * The part of an observable array contract that do not mutate the underlying value - see ReadableObservable type for rationale + */ +export interface ReadonlyObservableArrayFunctions extends ReadonlyObservableFunctions { //#region observableArray/generalFunctions /** * Returns the index of the first occurrence of a value in an array. @@ -102,7 +124,25 @@ export interface ObservableArrayFunctions extends ObservableFunctions number): T[]; + //#endregion +} +export interface ObservableArrayFunctions extends ReadonlyObservableArrayFunctions, ObservableFunctions { + //#region observableArray/generalFunctions /** * Removes the last value from the array and returns it. */ @@ -136,17 +176,6 @@ export interface ObservableArrayFunctions extends ObservableFunctions number): T[]; /** * Replaces the first value that equals oldItem with newItem * @param oldItem Item to be replaced @@ -161,7 +190,7 @@ export interface ObservableArrayFunctions extends ObservableFunctions boolean): T[]; @@ -203,7 +232,9 @@ export interface ObservableArrayFunctions extends ObservableFunctions extends Observable, ObservableArrayFunctions { +export interface ReadonlyObservableArray extends ReadonlyObservable, ReadonlyObservableArrayFunctions {} + +export interface ObservableArray extends Observable, ReadonlyObservableArray, ObservableArrayFunctions { (value: T[] | null | undefined): this; } @@ -217,48 +248,60 @@ export function isObservableArray(instance: any): instance is Observabl //#endregion -//#region subscribables/dependendObservable.js +//#region subscribables/dependentObservable.js export type ComputedReadFunction = Subscribable | Observable | Computed | ((this: TTarget) => T); export type ComputedWriteFunction = (this: TTarget, val: T) => void; -export type MaybeComputed = T | Computed; +export type MaybeComputed = T | Computed | WritableComputed; export interface ComputedFunctions extends Subscribable { // 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; + peek(): T; isActive(): boolean; getDependenciesCount(): number; getDependencies(): Subscribable[]; } +export interface WritableComputedFunctions extends ComputedFunctions { +} +/** A standard computed observable, which is read-only */ export interface Computed extends ComputedFunctions { (): T; +} +/** A writable computed observable, created with the "write" option */ +export interface WritableComputed extends WritableComputedFunctions, Computed { (value: T): this; } export interface PureComputed extends Computed { } +export interface WritablePureComputed extends WritableComputed { } export interface ComputedOptions { read?: ComputedReadFunction; - write?: ComputedWriteFunction; owner?: TTarget; pure?: boolean; deferEvaluation?: boolean; disposeWhenNodeIsRemoved?: Node; disposeWhen?: () => boolean; } +export interface WritableComputedOptions extends ComputedOptions { + write: ComputedWriteFunction; +} +export function computed(options: WritableComputedOptions): WritableComputed; export function computed(options: ComputedOptions): Computed; export function computed(evaluator: ComputedReadFunction): Computed; export function computed(evaluator: ComputedReadFunction, evaluatorTarget: TTarget): Computed; +export function computed(evaluator: ComputedReadFunction, evaluatorTarget: TTarget, options: WritableComputedOptions): WritableComputed; export function computed(evaluator: ComputedReadFunction, evaluatorTarget: TTarget, options: ComputedOptions): Computed; export module computed { - export const fn: ComputedFunctions; + export const fn: WritableComputedFunctions; } +export function pureComputed(options: WritableComputedOptions): WritablePureComputed; export function pureComputed(options: ComputedOptions): PureComputed; export function pureComputed(evaluator: ComputedReadFunction): PureComputed; export function pureComputed(evaluator: ComputedReadFunction, evaluatorTarget: TTarget): PureComputed; @@ -280,7 +323,7 @@ export interface ComputedContext { export const computedContext: ComputedContext; /** - * Executes a function and returns the result, while disabling depdendency tracking + * Executes a function and returns the result, while disabling dependency tracking * @param callback - the function to execute without dependency tracking * @param callbackTarget - the `this` binding for `callback` * @param callbackArgs - the args to provide to `callback` diff --git a/package-lock.json b/package-lock.json index 44825a1e6..77edcb520 100644 --- a/package-lock.json +++ b/package-lock.json @@ -338,9 +338,9 @@ "dev": true }, "typescript": { - "version": "3.7.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.5.tgz", - "integrity": "sha512-/P5lkRXkWHNAbcJIiHPfRoKqyd7bsyCma1hZNUGfn20qm64T6ZBlrzprymeu918H+mB/0rIg2gGK/BXkhhYgBw==", + "version": "3.9.9", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", + "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", "dev": true }, "underscore": { diff --git a/package.json b/package.json index 25a6d5562..fe7fe49a9 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,6 @@ "closure-compiler": "~0.2.1", "grunt": "~0.4.1", "grunt-cli": "~0.1.0", - "typescript": "^3.7.5" + "typescript": "^3.9.9" } } diff --git a/spec/templatingBehaviors.js b/spec/templatingBehaviors.js index f5f725cfd..730a9ee7c 100644 --- a/spec/templatingBehaviors.js +++ b/spec/templatingBehaviors.js @@ -216,7 +216,7 @@ describe('Templating', function() { ko.applyBindings(null, testNode); expect(testNode.childNodes[0].innerHTML).toEqual("template output"); }); - + it('can data-bind to blank name and displays no content', function () { // See #2446, #2534 testNode.innerHTML = "
" diff --git a/spec/types/module/test-readonly.ts b/spec/types/module/test-readonly.ts new file mode 100644 index 000000000..d6d1023d8 --- /dev/null +++ b/spec/types/module/test-readonly.ts @@ -0,0 +1,67 @@ +import * as ko from "knockout"; + +function testReadonlyObservable() { + const write = ko.observable("foo"); + write("bar"); + const read: ko.ReadonlyObservable = write; + + read(); // $ExpectType string + read.subscribe(() => { }); // Can still subscribe + // @ts-expect-error - But can't write to it + read("foo"); + + const writeAgain = read as ko.Observable; + writeAgain("bar"); +}; + +function testReadonlyObservableArray() { + // Normal observable array behavior + const write = ko.observableArray(["foo"]); + write(["bar"]); + write.push("foo"); + + // Readonly observable array + const read: ko.ReadonlyObservableArray = write; + read(); //$ExpectType ReadonlyArray + read.slice(0, 1); //$ExpectType string[] + + // @ts-expect-error + read(["foo"]); + // @ts-expect-error + read.push("bar"); + + // Can cast back to a writeable + const writeAgain = read as ko.ObservableArray; + writeAgain(["foo"]); +} + +function testReadonlyComputed() { + const write: ko.WritableComputed = ko.computed({ + read: () => "bar", + write: (x) => { }, + }); + + write("foo"); + + // Can cast a computed as readonly + const read: ko.Computed = write; + read(); + // @ts-expect-error + read("bar"); + + const normal1 = ko.computed({ read: () => "bar" }); + // @ts-expect-error + normal1("foo"); + + const normal2 = ko.computed(() => "bar"); + // @ts-expect-error + normal2("foo"); + + const pure1 = ko.pureComputed({ read: () => "bar" }); + // @ts-expect-error + pure1("foo"); + + const pure2 = ko.pureComputed(() => "bar"); + // @ts-expect-error + pure2("foo"); +}