tscpp / knuckles

Modern development toolkit for Knockout 👊
https://knuckles.elsk.dev
MIT License
1 stars 0 forks source link

Narrow types of observables in conditions #132

Open tscpp opened 2 weeks ago

tscpp commented 2 weeks ago
import * as ko from 'knockout'

export default {
  name: ko.observable<string | undefined>(),
};
<!-- ko if: name() !== undefined -->
  Hello <span data-bind="text: name()"></span>!
                               ^
                               "string | undefined" is not assignable to "string"
<!-- /ok if -->
tscpp commented 2 weeks ago

Simply unwrapping the observable won't work because the observable is a function. According to typescript, functions are nondeterministic, meaning that we can't expect the same return value.

const foo = ko.observable<string | undefined>();

if (foo() !== undefined) {
  const bar = foo();
  bar // string | undefined
}

The if/ifnot bindings should return a new child context where it extracts all the truthy values from the observable.

declare function binding<T>(value: ko.Observable<T>): ko.Observable<Extract<T, {}>>;

const foo = ko.observable<string | undefined>();
const bar = binding(foo); // extracts thruthy values
const baz = bar();
baz // string
tscpp commented 2 weeks ago

This is a problem because we can't know what condition is passed to the if/ifnot bindings. In the below example, we expect name to be ko.Observable<string> since we checked if the value is not null.

<!-- ko if: name() !== null -->
  Hello <span data-bind="text: name"></span>!
<!-- /ko -->

The below example unfortunately does not work since foo will only recieve an overload. So the type will become (): string | undefined & (): string. The return value from this type is string | undefined. In conclusion, asserting the observable will not work.

const foo = ko.observable<string | undefined>();
assert<ko.Observable<string>>(foo);

const bar = foo();
bar // string | undefined

A solution to this would be to assert all instances of the observable in the descendants of the binding. The downside is that this would probably require a lot of work and slow down the transpiler even more.

const foo = ko.observable<string | undefined>();

const bar = foo() as string;
bar // string

Issue is open to suggestions!