-
Notifications
You must be signed in to change notification settings - Fork 350
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
Convert nodes.js to TypeScript #1748
Conversation
GeraldRequired Reviewers
Don't want to be involved in this pull request? Comment |
Size Change: +841 B (+0.1%) Total Size: 867 kB
ℹ️ View Unchanged
|
packages/kas/src/nodes.ts
Outdated
|
||
return Mul.handleDivide(a, b).simplify(); | ||
} | ||
} | ||
|
||
/* abstract symbol node */ | ||
class Symbol extends Expr { | ||
class Sym extends Expr { |
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 renamed this to Sym
so that I could use Symbol
for the prefixes types down below.
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.
Even though we could do something other than using Symbol
for the prefix types, I'd like avoid shadowing built-ins like this.
packages/kas/src/nodes.ts
Outdated
const NumNeg = new Int(-1).addHint("negate"); | ||
const NumSub = new Int(-1).addHint("subtract"); | ||
const NumDiv = new Int(-1).addHint("divide"); | ||
|
||
Num.Sqrt = new Rational(1, 2).addHint("root"); | ||
const NumSqrt = new Rational(1, 2).addHint("root"); | ||
|
||
Num.Zero = new Int(0); | ||
Num.One = new Int(1); | ||
Num.Ten = new Int(10); | ||
|
||
// set identities here | ||
Add.prototype.identity = Num.Zero; | ||
Mul.prototype.identity = Num.One; | ||
const NumZero = new Int(0); | ||
const NumOne = new Int(1); | ||
const NumTen = new Int(10); |
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.
This can't be added to the Num
class as static properties because Int
and Rational
depend on Num
being defined. We could probably drop the Num
prefix. Then we could export Zero
and One
directly here instead of down below.
8f51d21
to
a1aa77e
Compare
npm Snapshot: PublishedGood news!! We've packaged up the latest commit from this PR (00596dd) and published it to npm. You Example: yarn add @khanacademy/perseus@PR1748 If you are working in Khan Academy's webapp, you can run: ./dev/tools/bump_perseus_version.sh -t PR1748 |
0ae9cfb
to
f38b16a
Compare
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 going to have to circle back to this tomorrow. Just some initial thoughts for now.
return [a, b]; | ||
} | ||
|
||
function isExpr(arg: string | Expr): arg is Expr { |
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 a blocker, just a thought: would it be worth having these as like static methods? Like Expr.isInstance
? Then we could make it abstract and force children to implement it; that way each variation would have a builtin predicate? idk, maybe more trouble than its worth, I just noticed the expr instanceof Mul
below which made me think of 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.
I'm not sure if type predicates can be generic. I'll have to experiment with that.
@@ -98,25 +134,43 @@ class Expr { | |||
} | |||
|
|||
// an abstraction for chainable, bottom-up recursion | |||
recurse(method, ...passed) { | |||
recurse(method: string, ...passed: any[]): 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 see a lot of any
s, is that because we don't understand the types or because inheritance dictates what types will be used by different implementations?
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.
This method is highly dynamic which makes typing it correctly difficult. 😞 I think we may be able to make this better using method overloads, but I don't want to block these changes on that. I'll add a TODO here to tackle this later. The return type though at least gives us some amount of type safety.
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.
Another batch of comments. Will continue looking at this after my meetings.
[ note to self, I left off at
1503 // expr.terms should always have at least two terms
1504 const last = _.last(expr.terms)!;
]
var args = _.map(this.args(), function (arg) { | ||
return _.isString(arg) ? arg : arg[method].apply(arg, passed); | ||
return _.isString(arg) || _.isNumber(arg) |
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.
Could you talk a little about the switch from isString
to isString || isNumber
? I see that in a couple of places. For the most part we're just changing types, but this is changing logic right?
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.
Sometimes the args passed to node constructors can include numbers. This is the case for Num
and its subclasses. I'm not sure why the existing code hasn't caused issues previously. 🤷♂️
packages/kas/src/nodes.ts
Outdated
return this.recurse("normalize"); | ||
} | ||
|
||
// expands the expression | ||
expand() { | ||
expand(options?): Expr { |
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.
Since options
isn't being used, can it be omitted?
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 it can. I'll make this change.
@@ -293,7 +330,7 @@ class Expr { | |||
|
|||
// raise this expression to a given exponent | |||
// most useful for eventually implementing i^3 = -i, etc. | |||
raiseToThe(exp) { | |||
raiseToThe(exp: Expr, options?: {preciseFloats?: boolean}): Expr { |
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.
Same here: do we need options
if it's never used? Or do we need it in order to tell TS that this is implementing an abstract class or something?
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 need it here because one of the subclasses uses it.
needsExplicitMul() { | ||
return this.args()[0].needsExplicitMul(); | ||
needsExplicitMul(): boolean { | ||
return this.exprArgs()[0].needsExplicitMul(); |
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.
Is this essentially the same, just more explicit?
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.
Without it TS thinks the return type is any
. Sometimes TS type inference is just really bad. I tried turning on noImplicitAny: true
in the tsconfig.json but then it was complaining about things that it was able to infer as a real type. 😞
term instanceof Rational && | ||
!(term instanceof Int) && |
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.
nit: I kind of like having hasDenom
as I think it makes intent clearer. Wouldn't lose sleep either way though.
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 hear you. TS unfortunately isn't able to follow the logic when it's expressed that way.
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.
Insomuch as I understand KAS, I think this PR makes sense. Thanks for doing all of this! I just ask that you don't land this right before going on sabbatical because I'm not sure any LEMS people understand this as much as you. 😂
@@ -1582,7 +1620,7 @@ export class Pow extends Expr { | |||
// If Float, convert to a Rational to enable the logic below | |||
if (simplifiedExp instanceof Float) { | |||
var num = simplifiedExp.n; | |||
var decimals = (num - num.toFixed()).toString().length - 2; | |||
var decimals = (num - +num.toFixed()).toString().length - 2; |
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.
This is just explicit coercion rather than implicit?
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.
That's correct.
// a Num here but tbh I'm not sure how this code isn't causing | ||
// an infinite loop. |
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.
tbh I'm not sure how this code isn't causing an infinite loop.
lol, comforting
const [base, exp] = | ||
factor instanceof Pow | ||
? [factor.base, factor.exp] | ||
: [factor, NumOne]; |
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.
nit so feel free to disregard, but IMO it's weird to create objects/arrays just to destructure from them. I like the original, but this is also an option (and shorter):
const [base, exp] = | |
factor instanceof Pow | |
? [factor.base, factor.exp] | |
: [factor, NumOne]; | |
const factorIsPow = factor instanceof Pow; | |
const base = factorIsPow ? factor.base : factor; | |
const exp = factorIsPow ? factor.exp : NumOne; |
This is just a personal preference thing. Not going to lose sleep on 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.
I don't think TS will refine factor
to be a Pow
correctly if we do that. We'd have to repeat the factor instanceof Pow
in each ternary.
super(); | ||
this.base = base; | ||
this.power = power; | ||
this.hints = _.extend(this.hints, { | ||
this.hints = { | ||
...this.hints, |
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.
These are hints from the parent class?
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.
That's correct. I believe that Expr
sets parens: false
or something like that.
@@ -3553,13 +3615,15 @@ export const parse = function (input, options) { | |||
var expr = parser.parse(input).completeParse(); | |||
return {parsed: true, expr: expr}; | |||
} catch (e) { | |||
return {parsed: false, error: e.message}; | |||
return {parsed: false, error: (e as Error).message}; |
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.
Is this possible? I feel like probably not or else I imagine you would have done this.
} catch (e: Error) {
return {parsed: false, error: e.message;
}
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.
TS does not allow this unfortunately. Part of the reason for that is that you can throw things other than Error
s (or Error
subclasses).
…ction with fat arrow in a bunch of places
77b359f
to
d9d350e
Compare
This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @khanacademy/[email protected] ### Patch Changes - [#1747](#1747) [`81ee69b0a`](81ee69b) Thanks [@kevinb-khan](https://github.com/kevinb-khan)! - Update nodes.js to use ES6 classes - [#1748](#1748) [`93bd39b6b`](93bd39b) Thanks [@kevinb-khan](https://github.com/kevinb-khan)! - Convert nodes.js to use TypeScript ## @khanacademy/[email protected] ### Patch Changes - [#1751](#1751) [`c95d08056`](c95d080) Thanks [@Myranae](https://github.com/Myranae)! - Refine InputNumber's rubric type - [#1756](#1756) [`3a208ba12`](3a208ba) Thanks [@Myranae](https://github.com/Myranae)! - Refine LabelImage's Rubric type - [#1762](#1762) [`a0f438fd7`](a0f438f) Thanks [@mark-fitzgerald](https://github.com/mark-fitzgerald)! - BUGFIX - [Number Line] - Some exercises with fractions wouldn't render - Updated dependencies \[[`81ee69b0a`](81ee69b), [`93bd39b6b`](93bd39b)]: - @khanacademy/[email protected] ## @khanacademy/[email protected] ### Patch Changes - Updated dependencies \[[`81ee69b0a`](81ee69b), [`c95d08056`](c95d080), [`93bd39b6b`](93bd39b), [`3a208ba12`](3a208ba), [`a0f438fd7`](a0f438f)]: - @khanacademy/[email protected] - @khanacademy/[email protected] ## @khanacademy/[email protected] ### Patch Changes - Updated dependencies \[[`81ee69b0a`](81ee69b), [`93bd39b6b`](93bd39b)]: - @khanacademy/[email protected] Author: khan-actions-bot Reviewers: mark-fitzgerald Required Reviewers: Approved By: mark-fitzgerald Checks: ⏭️ Publish npm snapshot, ✅ Check builds for changes in size (ubuntu-latest, 20.x), ✅ Cypress (ubuntu-latest, 20.x), ✅ Check for .changeset entries for all changed files (ubuntu-latest, 20.x), ✅ Lint, Typecheck, Format, and Test (ubuntu-latest, 20.x), ✅ gerald Pull Request URL: #1755
Summary:
After updating nodes.js to ues ES6 classes in #1747, the move to TypeScript became much more feasible. While this PR mostly just adds types in a bunch of places, there are some changes to the code itself in order to minimize the number of casts and @ts-expect-errors that were require. I've tried to keep these kinds of changes to a minimum.
I ended up converting a bunch of function expressions to arrow functions. Hopefully that doesn't add too much additional overhead when reviewing. There's still more cleanup that could be done (adding more missing types, converting all
var
s to eitherconst
orlet
, etc.), but I'm going to leave that for a future PR.Issue: None
Test plan: