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

Typescript) fix errors #202

Open
wants to merge 249 commits into
base: main
Choose a base branch
from
Open

Typescript) fix errors #202

wants to merge 249 commits into from

Conversation

phillipc
Copy link
Member

@phillipc phillipc commented Jan 12, 2025

As announced, we have started to make TKO more type-safe. To get a startpoint, we have set the tsconfig as softly as possible and continue to allow "any" types.

We (@Auge19 , @mcselle ) validate the typescript code with tsc. The current status is only 313 errors left. We began with 7410 errors. With this draft PR we would like to get feedback and start our own review. The goal is 0 errors with tsc. If this PR is ready and merged, we would like to continue with ESLINT.

All tests continue to pass.

phillipc and others added 30 commits December 20, 2024 21:45
add some types
#we should remove jasmine..
switch d.ts-version for jasmine (the project used jasmine 1.3)
1) [] => new Array
2) childNodes => children
3) extend some global type-defs
fix wrong jasmine imports
fix method signature
Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

👍 Great changes thank you. Will keep checking back and reviewing!

options: {
bindingGlobals: defaultOptions.global,
bindingStringPreparsers: [functionRewrite]
}
})

// @ts-ignore: Build-Parameter
Copy link
Member

Choose a reason for hiding this comment

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

also declare const

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

expect(clickedVM).toEqual(vm());

// set the view-model to a new object
vm({ someProp: observableConstructor('My new prop value'), checkVM: checkVM });
expect(testNode.childNodes[0].childNodes[0].value).toEqual('My new prop value');
expect(child.value).toEqual('My new prop value');
Copy link
Member

Choose a reason for hiding this comment

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

To make sure it has the latest DOM structure, child should be a function i.e.

const child = () => testNode.childNodes[0].childNodes[0] as HTMLInputElement

... because part of the assertion here is the DOM structure i.e. testNode.childNodes[0].childNodes[0] may no longer be child here, and if that were the case we'd want this test to fail. That failure wouldn't be visible if input is a reference to the input.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@phillipc
Copy link
Member Author

phillipc commented Mar 5, 2025

@brianmhunt: The reactivation of CodeQL worked so well that it directly filled all PRs with warnings. For now, I left it at a trigger via cron and deactiviated it on any PR. We would first have to configure a “meaningful” baseline for CodeQL.

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

Some more comments, great progress, thank you! I will return for more

@@ -27,7 +27,8 @@ import {bindings as coreBindings} from '@tko/binding.core'
import '@tko/utils/helpers/jasmine-13-helper'

describe('Binding: If', function () {
beforeEach(jasmine.prepareTestNode)
var testNode : HTMLElement
Copy link
Member

Choose a reason for hiding this comment

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

prefer let to avoid hoisting?

Copy link
Member Author

Choose a reason for hiding this comment

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

I would fix this globally with eslint in another PR afterwards. It's already prepared in @Auge19's branch.

@@ -26,7 +26,8 @@ import {
import '@tko/utils/helpers/jasmine-13-helper';

describe('else inside an if binding', function () {
beforeEach(jasmine.prepareTestNode);
var testNode : HTMLElement
Copy link
Member

Choose a reason for hiding this comment

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

prefer let (not a blocker, just noting)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@@ -281,7 +318,7 @@ export class ForEachBinding extends AsyncBindingHandler {
if (as) {
return v => this._contextExtensions($ctx.extend({ [as]: v }))
} else {
return v => $ctx.createChildContext(v, null, ctx => this._contextExtensions(ctx))
return v => $ctx.createChildContext(v, undefined, ctx => this._contextExtensions(ctx))
Copy link
Member

Choose a reason for hiding this comment

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

I assume our tests will cover the consequences of this, and that it's fine, but just noting that it's a somewhat semantic change in fairly fickle code. Probably ok, but noting just in case.

Copy link
Member Author

Choose a reason for hiding this comment

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

It should be covered. I'm always a bit unsure about this situations in JS. Is there a general rule like "undefinied is better than null" in JS/TS-Styling-Guides?

Copy link
Member

Choose a reason for hiding this comment

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

The general rule is that they are different and fickle and all a bit insane e.g.

undefined == null is true // equality with type-cast
undefined === null is false // strictly inequal
null + 1 is 1
undefined + 1 is NaN

function x (def = 1) { return def }
x(null) is null
x(undefined) is 1

typeof null is "object"
typeof undefined is "undefined"

So it's often dangerous to change old code from null to undefined and vice versa because of these (and many more) subtle differences that may not be immediately apparent, particularly if they're not covered by tests.

If we can keep this as null, that'd be safer.

Copy link
Member Author

Choose a reason for hiding this comment

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

I looked at the specific location again. The type definition is taken from the official "knockout.d.ts": dataItemAlias?: string The value is also "safely" handled with if (dataItemAlias) L116 at bindingContext.ts.

At this point I would call it a type error in foreach.ts. In general, however, I agree with you, we have to validate something like this case-by-case. Unfortunately JS allowed both. If you like, we can leave it at dataItemAlias?: string | null.

deleted: any[]
}

interface ChangeAddItem{
Copy link
Member

Choose a reason for hiding this comment

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

A stronger type here is:

interface ChangeAddBatchItem {
  status: 'added'
  index: number
  isBatch: true
  values?: any[]
}

interface ChangeAddOneItem {
  status: 'added'
  index: number
  isBatch?: false
  value: any
}

type ChangeAddItem = 
  | ChangeAddBatchItem
  | ChangeAddOneItem

then one can typeguard on this i.e.

const c: ChangeAddItem = ...
if (c.isBatch) {
  // c is ChangeAddBatchItem
  c.values
  c.value // TypeError
} else {
  // c is ChangeAddOneItem
  c.value
  c.values // TypeError
}

Not strictly needed, but good to have

https://www.typescriptlang.org/play/?#code/FASwdgLgpgTgZgQwMZQAQGEAWCwHMoCCAJkQEIIRKYCS0AtqgN7CqoDOEFArmwFyoByBCShEBLVOCJQAHvzBc6AI1gSQbcpUz8IMLlAkA3BABt9bAPz8cATwDaAXWABfYKEixEKDNjyESAPJgULRQDMysHNx8gsLSYmpg0nKoCsqqrOqaVFaoiCZsBqzGZlDWYDYubhA2AA5oWDj4xEShDAC8qBIAPj5N-mQUVG09fX4tQSH0bkgA9mAcqEj8jeMkbaidjM6oCGy7FaBwqAAUSAB0WUOYAJRMEgD0D0uS+6vNJNk006wXJfqoJ6oAAqdSgAFEYDBZjAJH9TOZAc9ZgBrFyoKAFNARJEvdRjD5ESYjX7nf5oIGouFkhFQfZA0H1SHQ2HOIA

Choose a reason for hiding this comment

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

Done

// Utilities
const MAX_LIST_SIZE = 9007199254740991

// from https://github.com/jonschlinkert/is-plain-object
function isPlainObject (o) {
function isPlainObject (o): o is Object {
Copy link
Member

Choose a reason for hiding this comment

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

The canonical object type is o is Record<string, any>, since Object is the parent of Array, Function, and many other classes.

Choose a reason for hiding this comment

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

Done

watchForSelectionChangeEvent (element, ieUpdateModel) {

// All variables are not found!
watchForSelectionChangeEvent (element?) {
Copy link
Member

Choose a reason for hiding this comment

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

it's unclear what's happening here and it doesn't seem related to the change of types so we might consider a separate PR to make sure the implications are well understood.

Could we break this particular change into a separate PR?

Copy link
Member Author

Choose a reason for hiding this comment

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

At this point we noticed through the typing that the old IE 8/9 logic could not have worked at all. We made it tsc compliant for now and added a FIXME maker.

I think that we can in a later separate pr, remove all IE8 to IE10-Logic. IE11 could be discussed.

Copy link
Member Author

@phillipc phillipc Mar 6, 2025

Choose a reason for hiding this comment

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

element can be undefinied , handler is never defined, ieUpdateModel will not be handed over

export default {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext: BindingContext) { // allBindings and viewModel not used!
Copy link
Member

Choose a reason for hiding this comment

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

We could use the convention to start unused vars with _ e.g.

  init: function (element, valueAccessor, _allBindings, _viewModel, bindingContext: BindingContext) {

then the comment isn't needed and in many editors the variable will have a different color for the unused var

Copy link
Member Author

Choose a reason for hiding this comment

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

@M-Kirchhoff has this adapted.

Actually, we should collect such things in a general TKO style guide.

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

I'm up to 55/193 files, making progress. Thank you!

@@ -33,7 +33,8 @@ import '@tko/utils/helpers/jasmine-13-helper';
describe('Binding dependencies', function () {
var bindingHandlers

beforeEach(jasmine.prepareTestNode);
var testNode : HTMLElement
Copy link
Member

Choose a reason for hiding this comment

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

prefer let

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, changed in 44 files ;) Let's do the rest later with eslint, it can be automated.

self.$parent = parentContext.$data
self.$parents = (parentContext.$parents || []).slice(0)
self.$parent = parentContext?.$data
self.$parents = (parentContext?.$parents || []).slice(0)
Copy link
Member

Choose a reason for hiding this comment

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

I assume $parents cannot be a falsy e.g. false or 0, but to be safe it's generally better to use ...?.$parents ?? []

(not needed, just noting)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

@@ -147,30 +196,30 @@ Object.assign(bindingContext.prototype, {
extend (properties) {
// If the parent context references an observable view model, "_subscribable" will always be the
// latest view model object. If not, "_subscribable" isn't set, and we can use the static "$data" value.
return new bindingContext(inheritParentIndicator, this, null, function (self, parentContext) {
return new bindingContext(inheritParentIndicator, this, undefined, function (self, parentContext) {
Copy link
Member

Choose a reason for hiding this comment

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

I'm assuming this has no material difference, but it's semantic and not just a type change, so we want to be mindful of knock-on consequences.

Copy link
Member Author

Choose a reason for hiding this comment

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

// We can only do something meaningful for elements and comment nodes (in particular, not text nodes, as IE can't store domdata for them)
if (node && (node.nodeType === 1 || node.nodeType === 8)) {
return storedBindingContextForNode(node)
}
}

export function dataFor (node) {
export function dataFor<T = any>(node: Node): T | undefined {
Copy link
Member

Choose a reason for hiding this comment

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

I'm not clear on what this generic does. Which is ok, and maybe I'll see it when it's used, but just noting.

Copy link
Member Author

Choose a reason for hiding this comment

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

Comes from the official d.ts files.. Only for the return value relevant. We intended to keep things as similar as possible. In reality, we had to compromise to the current state of implementation.

Copy link
Member Author

Choose a reason for hiding this comment

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

With make dts we can already generate definitions quite well. We would just need a delivery process later.

triggerEvent(testNode.childNodes[0], 'click')
triggerEvent(testNode.children[0], 'click')
triggerEvent(testNode.children[0], 'click')
triggerEvent(testNode.children[0], 'click')
Copy link
Member

Choose a reason for hiding this comment

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

I think all these .children[0] are ok replacing .childNodes[0]. The only case where there'd be a false positive is if there were two nodes and .childNodes[0] !== .children[0] and clicking on the .children[0] created a false positive. That doesn't seem likely here, so I'm ok with it, just noting.

Copy link
Member Author

Choose a reason for hiding this comment

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

alright, triggerEvent needs an element

@@ -281,7 +318,7 @@ export class ForEachBinding extends AsyncBindingHandler {
if (as) {
return v => this._contextExtensions($ctx.extend({ [as]: v }))
} else {
return v => $ctx.createChildContext(v, null, ctx => this._contextExtensions(ctx))
return v => $ctx.createChildContext(v, undefined, ctx => this._contextExtensions(ctx))
Copy link
Member

Choose a reason for hiding this comment

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

The general rule is that they are different and fickle and all a bit insane e.g.

undefined == null is true // equality with type-cast
undefined === null is false // strictly inequal
null + 1 is 1
undefined + 1 is NaN

function x (def = 1) { return def }
x(null) is null
x(undefined) is 1

typeof null is "object"
typeof undefined is "undefined"

So it's often dangerous to change old code from null to undefined and vice versa because of these (and many more) subtle differences that may not be immediately apparent, particularly if they're not covered by tests.

If we can keep this as null, that'd be safer.

@@ -33,29 +27,29 @@ describe('Array to DOM node children mapping', function () {
}
setDomNodeChildrenFromArrayMapping(testNode, array, mapping)
expect(testNode.childNodes.length).toEqual(4)
Copy link
Member Author

Choose a reason for hiding this comment

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

@brianmhunt This is also a useful methodology to deal with the assertion problem in the ChildNodes/Children adaptation.

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

Ok I'm back to 37/193 files reviewed, so we're actually going in reverse here. :)

Github is not good at these huge PR reviews. Is there any way we could create a series of PRs based on the changes here?

Having looked through a lot, much of it seems safe, but we want to a comprehensive second set of eyes just in case. e.g. can you pick 5-10 files and create a PR for those? I'm open to other strategies, but getting this very-large PR merged all-at-once may be a challenge.

The only general change that I think I'd like to discuss is using new Array() instead of []. How strong are you on that / are there alternatives?

Just as a rubber duck I asked Claude AI why one would prefer new Array() over [] and it somewhat aligned with my own thinking. I might be missing something and am open to discussion, but it was on my mind so wanted to investigate / raise it.

CleanShot 2025-03-11 at 15 57 59@2x

Thank you!

@phillipc
Copy link
Member Author

phillipc commented Mar 11, 2025

@brianmhunt I think we going in reserve with the review because of doing changes while you reviewing it. The last suggested const-to-let-convertation hits 44 files. I suggest we hold off on further changes to this PR until your review is complete. We are currently very motivated and would like to achieve 100% TSC conformity with this PR so that further improvements to TKO are possible on this basis.

Regarding the array topic, it is a tick for now so that the type system works: push, length and co are defined like an any[]. Later, all arrays should be clearly declared: number[] or Array<number>(). This leads to the same result, number[] is shorter.

Update 1: [] is an never[] so we can only push never-types, see https://stackoverflow.com/questions/52423842/what-is-not-assignable-to-parameter-of-type-never-error-in-typescript

Update 2: In fact, these are different array variants. The following article examines this. https://medium.com/@julienetienne/different-types-of-arrays-in-javascript-and-when-to-use-them-77f7843b71de
I have also created a playground for this topic.

Eslint would also find this flaw if a length is not defined: https://eslint.org/docs/latest/rules/no-array-constructor - eslint can partly fix this automatically, but... i would like to leave the topic as it is in this PR and tackle it in a second step together with the ESLINT installation. It generates 156 new warnings, which we would then correct by defining the array in a type-safe manner as number[] or something similar. Can we stay this way? A convert to typeof any[] would be another solution..

image

@brianmhunt
Copy link
Member

brianmhunt commented Mar 12, 2025

@brianmhunt I think we going in reserve with the review because of doing changes while you reviewing it. The last suggested const-to-let-convertation hits 44 files. I suggest we hold off on further changes to this PR until your review is complete. We are currently very motivated and would like to achieve 100% TSC conformity with this PR so that further improvements to TKO are possible on this basis.

Ok that should work, as long as this branch stays frozen (and we can do PRs into this branch, but wait to merge them), this should prevent the need to re-review. If there are things to fix in this PR we can decide whether it happens here or in a chained PR.

Regarding the array topic, it is a tick for now so that the type system works: push, length and co are defined like an any[]. Later, all arrays should be clearly declared: number[] or Array<number>(). This leads to the same result, number[] is shorter.

Update 1: [] is an never[] so we can only push never-types, see https://stackoverflow.com/questions/52423842/what-is-not-assignable-to-parameter-of-type-never-error-in-typescript

Update 2: In fact, these are different array variants. The following article examines this. https://medium.com/@julienetienne/different-types-of-arrays-in-javascript-and-when-to-use-them-77f7843b71de I have also created a playground for this topic.

Eslint would also find this flaw if a length is not defined: https://eslint.org/docs/latest/rules/no-array-constructor - eslint can partly fix this automatically, but... i would like to leave the topic as it is in this PR and tackle it in a second step together with the ESLINT installation. It generates 156 new warnings, which we would then correct by defining the array in a type-safe manner as number[] or something similar. Can we stay this way? A convert to typeof any[] would be another solution..

Thanks for the summary, I see it's the noImplicitAny and strictNullChecks, which are both very useful here.

Leaving them in works for me, it'll be easy to find the new Array() b/c they're inherently findable. The only real cost is the additional bytes and weak typing from new Array, which we can in the future fix with const x: something[] = [], if we feel it's necessary. Thanks for walking through it.

I'll continue on in the review of this PR.

Thanks @phillipc

For future reference:

  1. https://stackoverflow.com/questions/49219531/why-was-the-never-type-introduced-in-typescript/49225093#49225093
  2. https://stackoverflow.com/questions/52423842/what-is-not-assignable-to-parameter-of-type-never-error-in-typescript

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

I'm up to 70/193 files reviewed. Thank you!

@@ -26,11 +26,14 @@ import { bindings as templateBindings } from '@tko/binding.template'
import { bindings as ifBindings } from '@tko/binding.if'

import '@tko/utils/helpers/jasmine-13-helper'
import { Provider } from '@tko/provider'
Copy link
Member

Choose a reason for hiding this comment

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

is this used? maybe import type { Provider } ...?

Copy link
Member Author

Choose a reason for hiding this comment

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

That's so right, see L756.

image

for (const [key, BindingHandlerClass] of bindingsGenerated) {
// Go through the sorted bindings, calling init and update for each
function reportBindingError (during, errorCaptured) {
const reportBindingError = function (during, errorCaptured) {
Copy link
Member

Choose a reason for hiding this comment

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

it seems odd to make this anonymous, so it won't report in stack traces. probably ok, though.

Copy link
Member Author

Choose a reason for hiding this comment

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

IMO such a function should even better be anonymous and const, otherwise there may be problems with the scope. https://stackoverflow.com/questions/750486/javascript-closure-inside-loops-simple-practical-example

@@ -29,33 +30,38 @@ describe('Binding: Enable/Disable', function () {
var myObservable = observable()
testNode.innerHTML = "<input data-bind='enable:myModelProperty()' />"
applyBindings({ myModelProperty: myObservable }, testNode)

expect(testNode.childNodes[0].disabled).toEqual(true)
var input = testNode.children[0] as HTMLInputElement
Copy link
Member

Choose a reason for hiding this comment

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

should be const. small thing, but just to prevent hoisting since it's used a bunch in the other functions here where it could impact outcomes

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in Auge19#4

@@ -40,8 +41,8 @@ describe('Binding: Selected Options', function () {

it('Should set selection in the SELECT node to match the model', function () {
var bObject = {}
var values = new observableArray(['A', bObject, 'C'])
var selection = new observableArray([bObject])
var values = observableArray(['A', bObject, 'C'])
Copy link
Member

Choose a reason for hiding this comment

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

should be const (not a blocker here, just noting)

@@ -28,70 +29,76 @@ describe('Binding: CSS style', function () {
})

it('Should be able to use standard CSS style name (rather than JavaScript name)', function () {
var myObservable = new observable('red')
var myObservable = observable('red')
Copy link
Member

Choose a reason for hiding this comment

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

also let. I'll just note that generally and we can review/revisit in the future, it's not blocking, just noting.

@@ -183,7 +186,7 @@ describe('Binding: Using', function () {
testNode.innerHTML = "<div data-bind='using: someitem'>text" +
"<!-- ko foreach: childprop --><span data-bind='text: $data'></span><!-- /ko --></div>"

var childprop = observableArray([])
var childprop: ObservableArray = observableArray([])
Copy link
Member

Choose a reason for hiding this comment

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

weird that the : ObservableArray is needed, I'd expect observableArray([]) to provide that

Copy link
Member Author

Choose a reason for hiding this comment

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

It is not necessary, but its results in ObservableArray<never>.

image

Right at this point would be var childprop = observableArray<string>([]). The current way results in ObservableArray<any>.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, changed to generic in Auge19#4

@phillipc
Copy link
Member Author

Thanks @brianmhunt for the current process and progress. I have collect some fruther task here: #211 . I would be very interested in your thoughts on this points.

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

👍 77/193

"noEmit": true,
"noEmit": true, //Deactivate "noEmit" to generating d.ts-File to 'builds/dts/dist' via "make dts"
"emitDeclarationOnly": true,
"outDir": "builds/dts/dist",
Copy link
Member

Choose a reason for hiding this comment

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

I feel like the builds/dts/dist may change depending on whether it's the knockout or the tko build, but also it's ok for now - just noting.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is just an poc for now. The normal build chain is not affected by these parameters. I'm still unclear on how best to build a typing package. Once the PR is complete, I'd like to try it with https://www.npmjs.com/package/esbuild-plugin-d.ts

Regarding the build pipeline, I would preferred something like a one file bundle for TKO and @types/tko, with an esm export: export var tko. Corresponds to browser.es6.js

Copy link
Member

Choose a reason for hiding this comment

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

Thanks, I'm not clear on the best build strategy either. It'd be nice if the only dependency were Typescript itself, for the typing.

On the @types/tko we should be ok, noting that the static global for the knockout and the tko build may be different, so we want to be mindful of that.

"noEmit": true, //Deactivate "noEmit" to generating d.ts-File to 'builds/dts/dist' via "make dts"
"emitDeclarationOnly": true,
"outDir": "builds/dts/dist",
/*Disable some typing feature from ts to detect first steps to a strong typed version*/
Copy link
Member

Choose a reason for hiding this comment

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

👍

return JSON.parse(jsonString) as T;
}
// Fallback on less safe parsing for older browsers
return (new Function('return ' + jsonString))() as T
Copy link
Member

Choose a reason for hiding this comment

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

Noting (aside) we should really rely on JSON as a polyfill, if it's not defined.

Copy link
Member Author

Choose a reason for hiding this comment

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

This new Function(string) is like evil eval.

Good point, probably this case is generally obsolete. I think it is for IE < 8 and old Safari browser.. https://caniuse.com/json

Copy link
Member

Choose a reason for hiding this comment

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

yeah I think it's safe/wise to pull this out. It opens a vector to arbitrary code execution - if someone can unset the global JSON the fallback is dangerous. We're relying on newer things than JSON, so this won't be the thing that breaks TKO.

set: function (name, value) {
options[name] = value
},
onError (e : Error) : void { throw e }
Copy link
Member

Choose a reason for hiding this comment

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

stylistically, the x:type is preferable to x : type, the latter looking a bit too much like a ternary. (not a blocker, just noting)


// jQuery will be automatically set to globalThis.jQuery in applyBindings
// if it is (strictly equal to) undefined. Set it to false or null to
// disable automatically setting jQuery.
jQuery: globalThis.jQuery,
jQuery : JQueryStatic | boolean | null = (globalThis as any).jQuery
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure it can ever be true, just false

  jQuery : JQueryStatic | false | null = (globalThis as any).jQuery

Nothing needed to change here, just noting

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in Auge19#4

//
// This becomes ko.options
// --
//
// This is the root 'options', which must be extended by others.
export class Options {
[key: string]: any;
Copy link
Member

Choose a reason for hiding this comment

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

do we want this? doesn't that make all the other types any?

Copy link
Member Author

@phillipc phillipc Mar 18, 2025

Choose a reason for hiding this comment

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

The declared ones win's, but I would preferred to remove the index access here. We have some hidden options in the codebase. Declaring those is the better way.

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in Auge19#4

import { Provider } from "@tko/provider";

interface CustomBindingGlobalProperties {
String;
Copy link
Member

Choose a reason for hiding this comment

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

unclear what String is

Copy link
Member Author

Choose a reason for hiding this comment

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

It was wrong, fixed it Auge19#4

interface CustomBindingGlobalProperties {
  [customBindingName: string]: any;
}

String and isObservable are only used as test-case-props in templatingBehaviors.ts

export function clonePlainObjectDeep (obj, seen) {
if (!seen) { seen = [] }
export function clonePlainObjectDeep (obj, seen?: any[]) {
if (!seen) { seen = new Array() }
Copy link
Member

Choose a reason for hiding this comment

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

aside: we could/should improve the perf. here with a Set instead of array for seen

Copy link
Member Author

Choose a reason for hiding this comment

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

I took a look at the method and it looks like it's not used anymore. Mark as deprecated?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, even just /** @deprecated */ (in a subsequent PR) would be nice here. It's possible someone is using it externally but it's not a core deliverable of TKO/knockout and trivial to do on your own so I think it'd be good to get rid of it if it's not being used internally.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

👍 115/193

@@ -174,21 +177,21 @@ describe('Templating', function () {
setTemplateEngine(new dummyTemplateEngine({ someTemplate: 'template output' }))
testNode.innerHTML = "<div data-bind='template:\"someTemplate\"'></div>"
applyBindings(null, testNode)
expect(testNode.childNodes[0].innerHTML).toEqual('template output')
expect((testNode.childNodes[0]as HTMLElement).innerHTML).toEqual('template output')
Copy link
Member

Choose a reason for hiding this comment

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

small thing: whitespace before as

Copy link
Member Author

Choose a reason for hiding this comment

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

oh, fixed in Auge19#4


elseChainSatisfied((unwrap(dataArray) || []).length !== 0)
elseChainSatisfied((unwrap(dataArray) || []).length !== 0);
Copy link
Member

Choose a reason for hiding this comment

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

it'd be nice if the style guide here highlighted and ideally removed unnecessary semicolons

Copy link
Member Author

Choose a reason for hiding this comment

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

I've already prepared prettier here. It's configured to maintain the existing style: https://github.com/Auge19/tko/tree/add_prettier

Personally, I would prefer globaly ending semicolons.

@@ -171,7 +179,7 @@ export function renderTemplate (template, dataOrBindingContext, options, targetN
// Ensure we've got a proper binding context to work with
var bindingContext = (dataOrBindingContext && (dataOrBindingContext instanceof BindingContextConstructor))
? dataOrBindingContext
: new BindingContextConstructor(dataOrBindingContext, null, null, null, { 'exportDependencies': true })
: new BindingContextConstructor(dataOrBindingContext, undefined, undefined, undefined, { 'exportDependencies': true })
Copy link
Member

Choose a reason for hiding this comment

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

possibly semantically relevant i.e. undefined !== null. Likely ok, but noting.

@@ -56,7 +56,7 @@ describe('Throttled observables', function () {
// Wait until after timeout
waitsFor(function () {
return notifiedValues.length > 0
}, 300)
}, "Timeout", 300)
Copy link
Member

Choose a reason for hiding this comment

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

it would be stronger to declare const waitsFor: (...) => ...

Copy link
Member Author

@phillipc phillipc Mar 18, 2025

Choose a reason for hiding this comment

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

waitsFor from Jasmine 1.3 is only declared with this three parameters:

declare function waitsFor(latchMethod: () => boolean, failureMessage?: string, timeout?: number): void;

Adding a creative failure messages like "Timeout" is compliant with the jasmine typings, although this magic always happens:

jasmine.Spec.prototype.waitsFor = function(latchFunction, optional_timeoutMessage, optional_timeout) {
  var latchFunction_ = null;
  var optional_timeoutMessage_ = null;
  var optional_timeout_ = null;

  for (var i = 0; i < arguments.length; i++) {
    var arg = arguments[i];
    switch (typeof arg) {
      case 'function':
        latchFunction_ = arg;
        break;
      case 'string':
        optional_timeoutMessage_ = arg;
        break;
      case 'number':
        optional_timeout_ = arg;
        break;
    }
  }

  var waitsForFunc = new jasmine.WaitsForBlock(this.env, optional_timeout_, latchFunction_, optional_timeoutMessage_, this);
  this.addToQueue(waitsForFunc);
  return this;
};

expect(function () {
observable.extend({deferred: false})
observable.extend(Object.assign({deferred: false}))
Copy link
Member

Choose a reason for hiding this comment

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

This is a no-op. i.e. Object.assign isn't doing anything here. It's harmless, but worth removing (either in this PR or the next)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, Auge19#4

// Convert value to uppercase
filters.uppercase = function (value) {
return sproto.toUpperCase.call(unwrap(value))
interface Filters {
Copy link
Member

Choose a reason for hiding this comment

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

many consumers of Knockout/TKO will extend the filters, so this type has to be exposed for others to extend it.

Copy link
Member Author

Choose a reason for hiding this comment

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

export is added, see Auge19#4

Copy link
Member

Choose a reason for hiding this comment

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

This looks to mostly be re-organizing, and I didn't spot any semantic changes but it was a lot of changes so I may have missed something. Please let me know if there were any semantic changes here, but otherwise it's all good.

Copy link
Member Author

@phillipc phillipc Mar 18, 2025

Choose a reason for hiding this comment

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

I have check this, too. Only the implementation style is changed.

} from '../dist'
} from '../src'

import { assert } from 'chai';
Copy link
Member

Choose a reason for hiding this comment

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

import order should have relative imports after vendor/absolute i.e. 'chai' before '../src'

Copy link
Member Author

Choose a reason for hiding this comment

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

Changed in Auge19#4 . It is only a stylistic pattern or is there another reason?

@@ -48,11 +50,12 @@ describe('KO LifeCycle', function () {
assert.isFunction(Child.prototype.anchorTo)
assert.isFunction(Child.prototype.dispose)
assert.isFunction(Child.prototype.addDisposable)
assert.isNotFunction(Child.prototype.mixInto)
// TODO: Fails with tsc, check if test is obsolte with typescript
// assert.isNotFunction(Child.prototype.mixInto)
Copy link
Member

Choose a reason for hiding this comment

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

Noted. Since it's unlikely to be used and a strange/rare pattern, this feels ok to me.

Copy link
Member Author

Choose a reason for hiding this comment

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

Alternatively we could use any or set ts-ignore for the isNotFunction check?


// For arrays, also respect toJSON property for custom mappings (fixes #278)
if (typeof rootObject['toJSON'] === 'function') { visitorCallback('toJSON') }
if (typeof rootObject['toJSON'] === 'function') { visitorCallback('toJSON') }
Copy link
Member

Choose a reason for hiding this comment

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

wrong indent?

Copy link
Member Author

Choose a reason for hiding this comment

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

Fixed in Auge19#4

Copy link
Member

@brianmhunt brianmhunt left a comment

Choose a reason for hiding this comment

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

up to 175/193 . Thanks for all the hard work here.

I've added a few comments, some are meaningful, but nothing strictly blocking.

The remaining files tend to be larger ones, so may take a bit each, but will turn to it as soon as I can. Thank you!

if (node.tagName === 'SLOT') {
const parent = node.parentNode
const slotName = node.getAttribute('name') || ''
const openNode = document.createComment(`ko slot: "${slotName}"`)
const closeNode = document.createComment('/ko')

if(!parent)
Copy link
Member

Choose a reason for hiding this comment

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

should use if (...) { ... } by convention / clarity / lint

Copy link
Member

Choose a reason for hiding this comment

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

(not a blocker, but noting)

@@ -38,6 +38,9 @@ import {

import * as coreBindings from '@tko/binding.core';

import { assert} from "chai"
Copy link
Member

Choose a reason for hiding this comment

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

these vendor imports should be above the "relative" imports (not a blocker)

Copy link
Member Author

Choose a reason for hiding this comment

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

Done in Auge19#4, i have also fixed unneeded imports and testseries closure. I think everything is only stylistic icing on the cake.

@@ -3,6 +3,8 @@ import {
MultiProvider
} from '../dist'

import { assert } from "chai"
Copy link
Member

Choose a reason for hiding this comment

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

should be before relative imports

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, Auge19#4

Copy link
Member

Choose a reason for hiding this comment

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

unclear in review whether it's ok to delete this, just noting. I imagine if the tests still run it's fine, but noting.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have merge this file with the central file: utils/helpers/jasmine-13-helper.ts. All globally declared custom matchers should be always available.

@@ -251,7 +251,7 @@ describe('Interpolation Markup bindings', function () {

jasmine.setNodeText(testNode, "hello {{'name'}}!");
applyBindings(null, testNode);
expect(testNode).toContainText('hello --name--!');
expect(testNode).toContainText('hello --name--!'); */
Copy link
Member

Choose a reason for hiding this comment

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

are we removing a test expect here? do we lose some coverage?

Copy link
Member Author

Choose a reason for hiding this comment

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

This test-case was already deactivited via xit, so we saved ourselves the type correction.

} from '@tko/utils'
import { subscribable, dependencyDetection } from '@tko/observable'
import { getObjectOwnProperty, tasks } from '@tko/utils'
import { Loader } from './loaders'
Copy link
Member

Choose a reason for hiding this comment

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

import type

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, see Auge19#4

// For testing purposes, we make this synchronous.
detachAndDispose (node) {
super.detachAndDispose(node)
cleanNode(node)
}

dispose () {
Copy link
Member

Choose a reason for hiding this comment

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

this is a no-op

@@ -1,6 +1,8 @@

import { memoization } from '../dist'

//import { } from "karma-jasmine";
Copy link
Member

Choose a reason for hiding this comment

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

unneeded

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, see Auge19#4

@@ -52,7 +53,9 @@ export function unmemoizeDomNodeAndDescendants (domNode, extraCallbackParamsArra
}
}

export function parseMemoText (memoText) {
export function parseMemoText (memoText : string | null) : string | null {
if(!memoText)
Copy link
Member

Choose a reason for hiding this comment

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

needs if () { ... }

Copy link
Member Author

Choose a reason for hiding this comment

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

Done, see Auge19#4

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants