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

feat(types): add type definitions #24

Closed
wants to merge 9 commits into from
Closed

feat(types): add type definitions #24

wants to merge 9 commits into from

Conversation

pgoldrbx
Copy link

@pgoldrbx pgoldrbx commented Feb 22, 2020

Description

Initial progress toward adding type definitions to the package. The utils module feels reasonably well satisfied, with a few questions. The DOM and HTML types are handled using lib.dom (is this done correctly?

Grammar used for reference: http://www.jsonml.org/syntax/

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Chore, documentation, cleanup

Related Issue

Motivation and Context

This library is being used in applications built with typescript. Those applications have begun defining JsonML types and interfaces locally when it would make more sense to move them here into this package.

How Has This Been Tested?

Screenshots (if appropriate):

Checklist:

  • My code follows the code style of this project.
  • I have updated the documentation (if required).
  • I have read the CONTRIBUTING document.
  • I have added tests to cover my changes.
  • All new and existing tests passed.

@pgoldrbx pgoldrbx added the help wanted Extra attention is needed label Feb 22, 2020
@pgoldrbx pgoldrbx requested a review from neilius February 22, 2020 19:33
@coveralls
Copy link

coveralls commented Feb 22, 2020

Pull Request Test Coverage Report for Build 140

  • 0 of 0 changed or added relevant lines in 0 files are covered.
  • No unchanged relevant lines lost coverage.
  • Overall coverage remained the same at 81.326%

Totals Coverage Status
Change from base Build 139: 0.0%
Covered Lines: 340
Relevant Lines: 393

💛 - Coveralls

[key: string]: any // @TODO - can this be anything but a string? it technically SHOULD be,
// but I'm not sure if we want to be that strict. maybe it should be primitives at least?
// or maybe we just leave it alone?
}
Copy link
Author

@pgoldrbx pgoldrbx Feb 22, 2020

Choose a reason for hiding this comment

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

  1. I'm unsure about naming in general - whether to be verbose like I've done here, or to be simple like Attributes and Node or Element etc

  2. attribute values are a bit tricky. Anything parsed from an HTML will be in string format, always. Any JsonML attributes converted back into HTML or DOM are coerced to strings.

    But does that mean that, in the interim, they must be strings? Also, I know there are plenty of use cases where just pragmatically the values have been modified and altered and are definitely NOT strings. I think I'm in favor of leaving this set to any rather than imposing strictness.

Copy link

@jeromecovington jeromecovington Feb 24, 2020

Choose a reason for hiding this comment

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

  1. I frequently import a more generally named type as a "namespaced" import name if need be, I think you be alright to name simply.
  2. It might be nice to see this winnowed down to something other than any. Perhaps a primitive union named Attribute?

Copy link
Author

Choose a reason for hiding this comment

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

  1. I've gone w/ the generic naming.
  2. For values, I've defined a type to centralize the issue but I'm still debating a bit. I suspect our application usage (appending things to JsonML structures) will potentially conflict with the strict value here. Pragmatically, I worry this could cause some problems for us in practice and I'm wary of implementing it. In theory, any value could be ok if it as a toString() implementation anyway. This library could export both types for consuming applications to optionally make use of? 🤷‍♂

Choose a reason for hiding this comment

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

  1. I'm suggesting, for example:
type Attribute = string | number | <other primitive types here>;

The problem I see with using any is you open the doors to object literals and on and on, things which will not, to your point, have a toString() implementation.

Choose a reason for hiding this comment

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

question: Ah...I see you have AttributeValueStrict now. Why are you not using it?

Copy link
Author

@pgoldrbx pgoldrbx Feb 26, 2020

Choose a reason for hiding this comment

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

I'm not using it because I think it will break our applications.

I'm pretty sure we are extending element attributes with objects within our applications, since we don't intend to transform the JsonML back into HTML or DOM. An example of a common use case might be an embedded content reference where later we merge in the related data structures directly with the attributes.

A likely use case, in the wild:

['inline-embed', { type: 'foo', relatedData: {/* some object */}, }]

Enforcing this type strictness is correct per the spec but might become a problem for us.

Copy link
Author

@pgoldrbx pgoldrbx Feb 29, 2020

Choose a reason for hiding this comment

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

I gave this some more thought. This fork is open-source and shouldn't care about how downstream applications. The type definitions should be strict and accurate. If we need to find some workarounds later, we can figure that out. I've updated the attribute value definition accordingly.

Update: still more struggle. I'm testing this and it does indeed cause a great deal of trouble. Far more than it's worth, I'd say. Once I define strict attribute values, this is required by the Element definition and used everywhere. As a result, if an application deviates from the expected attribute value, everything fails and the application won't be able to make use of these exported definitions (they will have to be duplicated and modified for application use).

Thoughts? I feel like the most strictly correct thing to do is to keep the strict definitions and then duplicate and modify the definitions file as needed by applications. However, I worry about the trade-off in value.

Choose a reason for hiding this comment

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

In my experience, its pretty common to have strict types imported as correctly typed by a lib and then extend, union, or partial them in my code. Here's a paired down example of how this might work in this case: Playground Link.

Copy link
Author

Choose a reason for hiding this comment

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

Right, agreed. The trouble is that the AttributeValue will be used as part of other type definitions in this file. That's where it all goes sideways.

Copy link

Choose a reason for hiding this comment

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

@jeromecovington @pgoldrbx yeah, I think for now we may just want to use any for pragmatic reasons since downstream consumers haven't adhered to the spec when performing manual transformations of the JsonML after the initial document conversion to JsonML from Markdown. If we're wanting to strictly adhere to the spec, we may want to do this in a two-phase approach, where the first is to publish a version using any here and then issue a breaking change that follows the spec, giving consumers a chance to adjust approach on those not-to-spec implementations

index.d.ts Outdated

export interface JsonMLNode extends Array<string | JsonMLAttributes | JsonMLChildNode> {
0: string;
}

This comment was marked as resolved.

This comment was marked as resolved.

This comment was marked as resolved.

Copy link
Author

Choose a reason for hiding this comment

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

I think I worked this out finally!

Choose a reason for hiding this comment

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

question: Do you actually need separate types here? Why not something like:

interface ElementMaybeWithAttributes extends Array<TagName | Attributes | Element | undefined> {
  0: TagName;
  1?: Attributes;
}


// Utils
const isFragYes: boolean = utils.isFragment(['', 'hi']);
const isFragNo: boolean = utils.isFragment(['div', 'hi']);
Copy link
Author

Choose a reason for hiding this comment

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

Is there a better way to assert types?

Choose a reason for hiding this comment

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

suggestion: You might like to take a look at using assertion functions. There are various options.

Choose a reason for hiding this comment

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

suggestion: Another less obvious option would be to add a test script which exercises the types using the TypeScript compiler. Here's an example from core-logbus-sdk.

Copy link
Author

Choose a reason for hiding this comment

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

I didn't make use of assert since (currently) this file is only run through tsc and not executed at runtime. Additionally, I guess I'd have to figure out how to specify assert/node types in the config?

I did look at other repo examples, and the logbus is another good reference
https://github.com/CondeNast-Copilot/core-logbus-sdk/blob/master/test/assert-types.ts

It feels somewhat similar to what I'm doing - which is essentially just trying to setup all the calls and objects. I went the extra difference of assigning the return value so the type checker would catch this also.

Copy link

@jeromecovington jeromecovington left a comment

Choose a reason for hiding this comment

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

Few initial thoughts and comments, hopefully they are useful!

[key: string]: any // @TODO - can this be anything but a string? it technically SHOULD be,
// but I'm not sure if we want to be that strict. maybe it should be primitives at least?
// or maybe we just leave it alone?
}
Copy link

@jeromecovington jeromecovington Feb 24, 2020

Choose a reason for hiding this comment

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

  1. I frequently import a more generally named type as a "namespaced" import name if need be, I think you be alright to name simply.
  2. It might be nice to see this winnowed down to something other than any. Perhaps a primitive union named Attribute?

index.d.ts Outdated

export interface JsonMLNode extends Array<string | JsonMLAttributes | JsonMLChildNode> {
0: string;
}

This comment was marked as resolved.

index.d.ts Outdated

export function addAttributes(jml: JsonMLNode, attr: JsonMLAttributes): undefined;

export function getAttribute(jml: JsonMLNode, key: string): any;

Choose a reason for hiding this comment

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

suggestion: Here, you could use the suggested Attribute as return type.

index.d.ts Outdated

export function getAttributes(jml: JsonMLNode, addIfMissing?: boolean): JsonMLAttributes;

export function addAttributes(jml: JsonMLNode, attr: JsonMLAttributes): undefined;

Choose a reason for hiding this comment

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

suggestion: I may be mistaken but I think the return value here would more properly be void. I see this elsewhere in the utils namespace as well.

Copy link
Author

Choose a reason for hiding this comment

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

oh yeah! thanks


// Utils
const isFragYes: boolean = utils.isFragment(['', 'hi']);
const isFragNo: boolean = utils.isFragment(['div', 'hi']);

Choose a reason for hiding this comment

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

suggestion: You might like to take a look at using assertion functions. There are various options.


// Utils
const isFragYes: boolean = utils.isFragment(['', 'hi']);
const isFragNo: boolean = utils.isFragment(['div', 'hi']);

Choose a reason for hiding this comment

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

suggestion: Another less obvious option would be to add a test script which exercises the types using the TypeScript compiler. Here's an example from core-logbus-sdk.

@pgoldrbx pgoldrbx force-pushed the feat/types branch 3 times, most recently from 140d314 to 5a51905 Compare February 29, 2020 05:22
//
// `Node` type imported from lib.dom
//
/// <reference lib="dom"/>
Copy link
Author

@pgoldrbx pgoldrbx Mar 9, 2020

Choose a reason for hiding this comment

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

Attempting to mark that lib.dom is required. The Node type is available as a global after adding 'dom' to tsconfig. I wish I could reference it more explicitly.

Overall, I'm not sure if I'm handling namespacing/scope properly.

I've also renamed types to avoid shadowing the DOM globals.

  • Element -> JSONMLElement
  • Node -> JSONMLNode

(based on similar from HTMLElement)

| [ TagName, Attributes ]
| [ TagName, ...JSONMLNode[] ]
| [ TagName, Attributes, ...JSONMLNode[] ]
;
Copy link
Author

Choose a reason for hiding this comment

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

@jeromecovington I've tried another approach! I think I have variable-length tuples working now by using spread. 🤷‍♂

['p', { foo: 'bar' }, 'some fragment text'],
['div', 'foo'],
'baz'
]);
Copy link
Author

Choose a reason for hiding this comment

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

Tested and can verify that I get issues if the attributes are moved to the wrong position or if an undefined or otherwise invalid element is added somewhere.

@pgoldrbx pgoldrbx changed the title feat(types): add type definitions (in progress) feat(types): add type definitions Mar 16, 2020
@pgoldrbx pgoldrbx marked this pull request as ready for review March 16, 2020 23:26
@pgoldrbx pgoldrbx added enhancement New feature or request and removed help wanted Extra attention is needed labels Mar 16, 2020
Copy link
Author

@pgoldrbx pgoldrbx left a comment

Choose a reason for hiding this comment

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

@neilius @jeromecovington could I get another review on this?

@pgoldrbx
Copy link
Author

pgoldrbx commented Feb 7, 2024

closing as abandoned

@pgoldrbx pgoldrbx closed this Feb 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants