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

Convert nodes.js to TypeScript #1748

Merged
merged 15 commits into from
Oct 17, 2024
Merged

Convert nodes.js to TypeScript #1748

merged 15 commits into from
Oct 17, 2024

Conversation

kevinb-khan
Copy link
Contributor

@kevinb-khan kevinb-khan commented Oct 13, 2024

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 vars to either const or let, etc.), but I'm going to leave that for a future PR.

Issue: None

Test plan:

  • yarn test packages/kas/src

@kevinb-khan kevinb-khan self-assigned this Oct 13, 2024
@khan-actions-bot khan-actions-bot requested a review from a team October 13, 2024 18:58
@khan-actions-bot
Copy link
Contributor

khan-actions-bot commented Oct 13, 2024

Gerald

Required Reviewers
  • @Khan/perseus for changes to .changeset/rare-pumas-rule.md, packages/kas/src/nodes.ts

Don't want to be involved in this pull request? Comment #removeme and we won't notify you of further changes.

Copy link
Contributor

github-actions bot commented Oct 13, 2024

Size Change: +841 B (+0.1%)

Total Size: 867 kB

Filename Size Change
packages/kas/dist/es/index.js 38.8 kB +841 B (+2.21%)
ℹ️ View Unchanged
Filename Size
packages/keypad-context/dist/es/index.js 760 B
packages/kmath/dist/es/index.js 4.27 kB
packages/math-input/dist/es/index.js 78 kB
packages/math-input/dist/es/strings.js 1.79 kB
packages/perseus-core/dist/es/index.js 1.48 kB
packages/perseus-editor/dist/es/index.js 280 kB
packages/perseus-linter/dist/es/index.js 22.2 kB
packages/perseus/dist/es/index.js 420 kB
packages/perseus/dist/es/strings.js 3.4 kB
packages/pure-markdown/dist/es/index.js 3.66 kB
packages/simple-markdown/dist/es/index.js 12.4 kB

compressed-size-action


return Mul.handleDivide(a, b).simplify();
}
}

/* abstract symbol node */
class Symbol extends Expr {
class Sym extends Expr {
Copy link
Contributor Author

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.

Copy link
Contributor Author

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 Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
Comment on lines 3615 to 3553
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);
Copy link
Contributor Author

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.

Copy link
Contributor

github-actions bot commented Oct 13, 2024

npm Snapshot: Published

Good news!! We've packaged up the latest commit from this PR (00596dd) and published it to npm. You
can install it using the tag PR1748.

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

Copy link
Contributor

@handeyeco handeyeco 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 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 {
Copy link
Contributor

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.

Copy link
Contributor Author

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.

packages/kas/src/nodes.ts Show resolved Hide resolved
@@ -98,25 +134,43 @@ class Expr {
}

// an abstraction for chainable, bottom-up recursion
recurse(method, ...passed) {
recurse(method: string, ...passed: any[]): this {
Copy link
Contributor

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 anys, is that because we don't understand the types or because inheritance dictates what types will be used by different implementations?

Copy link
Contributor Author

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.

@handeyeco handeyeco requested a review from a team October 15, 2024 21:32
Copy link
Contributor

@handeyeco handeyeco left a 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)!;

]

packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
var args = _.map(this.args(), function (arg) {
return _.isString(arg) ? arg : arg[method].apply(arg, passed);
return _.isString(arg) || _.isNumber(arg)
Copy link
Contributor

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?

Copy link
Contributor Author

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. 🤷‍♂️

return this.recurse("normalize");
}

// expands the expression
expand() {
expand(options?): Expr {
Copy link
Contributor

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?

Copy link
Contributor Author

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.

packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
@@ -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 {
Copy link
Contributor

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?

Copy link
Contributor Author

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();
Copy link
Contributor

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?

Copy link
Contributor Author

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. 😞

Comment on lines +919 to +931
term instanceof Rational &&
!(term instanceof Int) &&
Copy link
Contributor

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.

Copy link
Contributor Author

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.

packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
@kevinb-khan kevinb-khan requested a review from a team October 16, 2024 17:19
Copy link
Contributor

@handeyeco handeyeco left a 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;
Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's correct.

Comment on lines +1858 to +1859
// a Num here but tbh I'm not sure how this code isn't causing
// an infinite loop.
Copy link
Contributor

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

Comment on lines +1946 to +1949
const [base, exp] =
factor instanceof Pow
? [factor.base, factor.exp]
: [factor, NumOne];
Copy link
Contributor

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):

Suggested change
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.

Copy link
Contributor Author

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,
Copy link
Contributor

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?

Copy link
Contributor Author

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.

packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
@@ -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};
Copy link
Contributor

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;
    }

Copy link
Contributor Author

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 Errors (or Error subclasses).

packages/kas/src/nodes.ts Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
packages/kas/src/nodes.ts Outdated Show resolved Hide resolved
@kevinb-khan kevinb-khan changed the base branch from kas-class-nodes to main October 17, 2024 15:02
An error occurred while trying to automatically change base from kas-class-nodes to main October 17, 2024 15:02
@kevinb-khan kevinb-khan merged commit 93bd39b into main Oct 17, 2024
9 checks passed
@kevinb-khan kevinb-khan deleted the kas-ts-nodes branch October 17, 2024 15:20
mark-fitzgerald pushed a commit that referenced this pull request Oct 17, 2024
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
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.

3 participants