-
Notifications
You must be signed in to change notification settings - Fork 36
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
base: main
Are you sure you want to change the base?
Conversation
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A few more comments, looking really great. The main concern I've spotted is the loss of info changing assertions from .childNodes to .children. Would be grateful for some thoughts on that. Thank you, and I'll keep turning back to look at this
builds/knockout/src/index.ts
Outdated
new AttributeProvider() | ||
] | ||
}), | ||
provider: provider, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can use the shorthand provider,
in place of provider: provider,
, which would be consistent with filters,
below, and in any case shorter. Not needed, just noting
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
builds/reference/src/index.ts
Outdated
@@ -52,6 +54,7 @@ const builder = new Builder({ | |||
] | |||
}) | |||
|
|||
// @ts-ignore: Build-Parameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps this would work, stronger than an ignore?
declare const BUILD VERSION: string
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
o: () => ObservableArray // For jsxBehaviors Test | ||
} | ||
|
||
//Jasmine and Mocha define duplicated functions, is a problem for the type system |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, those or playwright.dev or bun.sh. They're pretty tightly tied to Jasmine right now, so it'd be an effort, but homogenizing the runner would help a lot.
@@ -244,14 +244,15 @@ describe('Deferred bindings', function () { | |||
|
|||
// Value is initially true, so nodes are retained | |||
applyBindings({ someItem: someItem, counter: 0 }, testNode) | |||
expect(testNode.childNodes[0].childNodes[0].tagName.toLowerCase()).toEqual('span') | |||
|
|||
expect(testNode.children[0].children[0].tagName.toLowerCase()).toEqual('span') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ah right, typing. I wonder if .childNodes, being broader, is a stronger test and we are introducing the possibility of false negative tests here i.e. missing failures where childNodes would have additional information that would reveal failures.
There are a few cases to consider:
- .childNodes having text/virtual nodes that it shouldn't and the test would fail, but .children test would still pass
- .children ignores/collapses important white-space that .childNodes picks up
Which is all to say, for the sake of type safety this would make the tests weaker and if/as we change TKO we may introduce bugs that the stronger variants may pick up.
Not to say I'm entirely opposed to the change, but wondering if perhaps there's an alternative
testNode.childNodes[0].childNodes[0]['tagName']['toLowerCase']()
Properly there'd be type guarding in place, but that'd be a heftier ask I feel.
Any thoughts come to mind on this concern?
packages/bind/src/bindingContext.ts
Outdated
} | ||
|
||
export interface BindingContext<T = any> { | ||
ko: any; // typeof ko; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
not needed here, but it would be nice to even have a placeholder like KnockoutInternalInstance = any
so we could track these down later and strengthen the types
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good point, done!
packages/bind/src/bindingContext.ts
Outdated
// The bindingContext constructor is only called directly to create the root context. For child | ||
// contexts, use bindingContext.createChildContext or bindingContext.extend. | ||
export function bindingContext (dataItemOrAccessor, parentContext, dataItemAlias, extendCallback, settings) { | ||
export const bindingContext = function<T>(dataItemOrAccessor: any, parentContext?: BindingContext, dataItemAlias?: string, extendCallback?: BindingContextExtendCallback<T>, settings?: BindingContextSetting) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
making the function anonymous means the name won't show up in stack traces. I'm not sure but we might be able to set bindingContext.name = 'bindingContext'
(or perhaps Object.defineProperty, but same idea) and then it's show up in stack traces, but I've never tested this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I will check this. This style of implementation allows the as unknown as bindingContext
magic, so the returned "objects" can be typed. If we convert this to proper TS/JS-classes in the future, this would be solved anyway.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. It's not a blocker IMO, just an observation since the stack trace is pretty well named already.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have been analyzing Knockout.js applications since 2015, this is an important point 💯
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I found an easy solution in Typescript:
export const bindingContext = function bindingContextFactory<T>(....)
This results in JS:
export const bindingContext = function bindingContextFactory(dataItemOrAccessor, parentContext, dataItemAlias, extendCallback, settings)
The name just has to be different, otherwise tsc will remove it!
There was a problem hiding this 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!
builds/knockout/src/index.ts
Outdated
options: { | ||
bindingGlobals: defaultOptions.global, | ||
bindingStringPreparsers: [functionRewrite] | ||
} | ||
}) | ||
|
||
// @ts-ignore: Build-Parameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
also declare const
There was a problem hiding this comment.
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'); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
@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. |
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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)) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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{ |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?) { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
packages/binding.core/src/let.ts
Outdated
export default { | ||
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) { | ||
init: function (element, valueAccessor, allBindings, viewModel, bindingContext: BindingContext) { // allBindings and viewModel not used! |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
prefer let
There was a problem hiding this comment.
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.
packages/bind/src/bindingContext.ts
Outdated
self.$parent = parentContext.$data | ||
self.$parents = (parentContext.$parents || []).slice(0) | ||
self.$parent = parentContext?.$data | ||
self.$parents = (parentContext?.$parents || []).slice(0) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
// 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 { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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') |
There was a problem hiding this comment.
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.
@@ -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)) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
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.