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): cleaning up our type definitions #632

Merged
merged 6 commits into from
May 22, 2021
Merged

Conversation

dbanksdesign
Copy link
Member

@dbanksdesign dbanksdesign commented May 13, 2021

Issue #, if available:

Description of changes:

  • Updating the typescript formats to use the updated format arguments
  • Using a shared DesignToken type for typescript/module-definitions format as well as in our types
  • Fixed a lot of missing things and incorrect things in our type definitions
  • Separated types out into their own files for readability

I'm not a Typescript person so would love some expert 👀 on this.

update
Here is what I am thinking for this pull request: is this code good enough for now? Good enough meaning: is it a better developer experience for Javascript and Typescript developers than what currently exists in the live version of Style Dictionary (2.10)? If so, and there are no obvious errors with the types, then I think we can merge in and get 3.0 out, and then plan on cleaning up in a minor release afterwards.

Some things that are not great, but acceptable for now to not push the 3.0 launch out even further to migrate the whole codebase to Typescript.

  • Duplication of JSDoc documentation. I think this is an acceptable interim solution for now. The alternative would either be to not have JSDoc comments in the typescript declarations, but that would be a worse developer experience than 2.10 because the Intellisense popups would show less information.

Other avenues I tried:

  • Using the tsc with the existing source JS files to auto-generate types. This worked somewhat, but it had some downsides. It did not work well trying to share types across different parts of the codebase, like a Transform or DesignToken for example. It also did not export all types in the main entry point to the library. To get this to work well, it would require a lot more tweaking and editing of the source code, which at that point we might as well migrate to typescript.

In maybe a new minor version (3.1) we could migrate the codebase to Typescript and clean this up a bit.

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

Copy link
Contributor

@luke-john luke-john left a comment

Choose a reason for hiding this comment

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

Really nice work on this 👍 , my comments are mostly stylistic notes.

disclaimer

I don't have access to a decent sized code base which uses style-dictionary, and so don't have a good way to test the type changes.

If you're able to pre publish a test version of these, @didoo may be able to run them against a codebase which would possibly pick up issues if they had been introduced.

suggestion for consideration, using these types in the js source files

Between these type files, the documentation website, and the jsdoc types in the source files, things are now being documented across three places 😅.

To help reduce this back towards two, you may want to consider replacing the jsdoc types in the source files with the types in these files. This would not just help keep the types up to date, but can also improve the development experience when working on style-dictionary.

ie. lib/cleanFiles.js (note: the params here assume the type files have had the default exports removed)

// this comment means you'll get type warnings in vscode (and if you wanted, could also run the typescript compiler on your source files to get some level of jsdoc type validation.
// @ts-check
/**
 * Takes a platform config object and a dictionary
 * object and cleans all the files. Dictionary object
 * should have been transformed and resolved before this
 * point.
 * @memberOf StyleDictionary
 * @param { import('../types/Dictionary').Dictionary } dictionary
 * @param { import('../types/Platform').Platform } platform
 * @returns { void }
 */
function cleanFiles(dictionary, platform) {

unrelated observation

And finally a note on something you haven't changed, but I noticed while looking at the index.d.ts file.

export as namespace StyleDictionary;

This is setting it up to behave as a UMD module (see https://www.typescriptlang.org/docs/handbook/modules.html#umd-modules). I'm not sure this is something that reflects the actual behaviour of styled-dictionary.

undo?(dictionary: Dictionary, config: Platform): void;
}

export default Action;
Copy link
Contributor

Choose a reason for hiding this comment

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

I've not encountered this pattern in typescript before where a type is exported via a default, and was surprised it worked.

While it does work, it's very unusual in typescript (I've never encountered this before).

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 my "non-typescript person trying to write typescript" coming out. What is the conventional way to do this then? I did have a lot of weirdness trying to split up the type definition files and import/export them. Would love to know the better way to do this.

Copy link
Contributor

Choose a reason for hiding this comment

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

While exploring the jsdoc setup, I've created a branch with the types adjusted to use named exports (and the definition refactored to a module) that you can check out.

https://github.com/amzn/style-dictionary/compare/fixing-typescript...luke-john:fixing-typescript-explore?expand=1

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 that is awesome! Thank you so much for taking the time and doing that. I really appreciate it.

thank you so much

interface Action {
do(dictionary: Dictionary, config: Platform): void;
undo?(dictionary: Dictionary, config: Platform): void;
}
Copy link
Contributor

@luke-john luke-john May 14, 2021

Choose a reason for hiding this comment

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

I see this is just isolating an existing type, so enhancing them is probably best left for a future pr.

However looking at https://amzn.github.io/style-dictionary/#/api?id=registeraction it seems this definition is incomplete (missing name). (this lack of completeness applies to many types still, I've not commented elsewhere)

An additional enhancement for consumers, would be to add jsdoc comments to help communicate usage to users. (again probably best left for a future pr)

ie.

interface Action {
  /** The action in the form of a function. */
  do(dictionary: Dictionary, config: Platform): void;
  /** A function that undoes the action. */
  undo?(dictionary: Dictionary, config: Platform): void;
}

(comments taken from https://amzn.github.io/style-dictionary/#/api?id=registeraction)

Copy link
Member Author

Choose a reason for hiding this comment

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

For all of the register-able things (action, format, filter, fileHeader, transform, transformGroup, parser) I isolated the thing itself (an action for example) from how you would register it. In Style Dictionary you can add custom things 2ways: with a register method, or by sticking it directly in a configuration object:

StyleDictionary.extend({
  action: {
    actionName: { do: () => {}, undo: () => {} }
  }
});

In this way, the "name" of the action is outside the action object, and is the key to that object. To handle both scenarios I created a Keyed and Named type helper so the Action interface can be shared for both. Does that make sense? There might be a better way to accomplish that.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sure, that makes sense.

source?: string[];
tokens?: DesignTokens;
properties?: DesignTokens;
platforms: Keyed<Platform>;
Copy link
Contributor

Choose a reason for hiding this comment

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

It's unclear to me if this is correct, on the website a number of these properties seem to be nested under Platform, and don't appear on Config (transform , transformGroup, fileHeader) and some are not documented at all (parsers, tokens).

I assume declaring the first group (ie. transform) here by name, allows you to use it inside platforms by name. Nitpick: Seeing as this is the first time it's being documented, it may be worth adding JSDoc comments to the types here explaining usage.

https://amzn.github.io/style-dictionary/#/config?id=configjson

Copy link
Member Author

Choose a reason for hiding this comment

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

They are not documented on the main branch, but have been updated in 3.0. If you look at my comment above, the .extend method can accept a transform object to define custom transforms inline in the configuration directly rather than calling .registerTransform. Some examples:

* and limitations under the License.
*/

//start
Copy link
Contributor

Choose a reason for hiding this comment

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

nitpick, you may want to add a comment before //start to indicate that it is being used by an internal tool (formats.js), so that additional changes in future keep that in mind.

*/

type Named<T> = T & { name: string; };
type Keyed<T> = { [name: string]: T };
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a note, typescript comes with a utility type Record which allows something similar.

So consumers could instead write Record<string, Type>.

Though, from looking at consumers, it seems this type is being used to indicate such maps where the key becomes an alias to the value that is available for usage elsewhere. nitpick: In which case adding a comment here, or giving a more descriptive name may help.

pattern: RegExp;
parse: (props: ParserOptions) => Properties;
}
declare namespace StyleDictionary {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just a note, the TypeScript docs recommend "modules over namespaces in modern code."

https://www.typescriptlang.org/docs/handbook/namespaces-and-modules.html#using-modules

To transform this file, you would need to

delete the namespace wrapper

- declare namespace StyleDictionary {
- }

update the types inside it to be exported

- type DesignToken = _DesignToken;
+ export type DesignToken = _DesignToken; 

Or, if you also removed the default exporting of types in many of the files, you could replace them so you just reexport the original types.

- import _File from "./File";
- type File = _File;
+ export { File } from "./File";

and finally update the main export.

- declare var StyleDictionary: StyleDictionary.Core;
- export = StyleDictionary;
+ declare var StyleDictionary: Core;
+ export default StyleDictionary;

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 knew doing this felt so wrong:

import _File from "./File";
type File = _File;

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 think we might need to revert back to the namespace style or else it might mess up the autocomplete/intellisense for JS users.

@dbanksdesign
Copy link
Member Author

Thank you for the comments @luke-john! 100% agree about having documentation spread in multiple places. I'll try your suggestion of importing the types in the JSDoc comments. Could we also put JSDoc comments in the type files and then reference those in the JS files? It is hard to strike a balance of adding types, but without re-writing the whole codebase 😓

@dbanksdesign
Copy link
Member Author

@luke-john I get a JSDoc error when I try to run JSDoc with the { import('') } syntax.

JSDOC_ERROR: ERROR: Unable to parse a tag's type expression for source file /Users/djb/Desktop/style-dictionary/lib/cleanFiles.js in line 16 with tag title "param" and text "{ import('../types/Dictionary').Dictionary } dictionary": Invalid type expression "import('../types/Dictionary').Dictionary": Expected "!", "=", "?", "[]", "|", or end of input but "(" found.

@luke-john
Copy link
Contributor

luke-john commented May 15, 2021

I hadn't noticed that you were using jsdoc-api to generate some of the website documentation when I made that comment.

I just had a look into it, and I can't find a simple way to get jsdoc to understand those types. There's a tracking issue on jsdoc for something similar, but it does not seem very active jsdoc/jsdoc#1645.

There are a number of alternatives, which will understand both jsdoc + typescript (such as typedoc), but that would be a fairly significant rework in and off itself. And while I'd be happy to help with migrating to such an alternative (or just going all in on typescript 😅), it would definitely be in "re-writing the whole codebase" territory 😓 .

Also you could import the js files jsdoc types into the types files. ie.

// index.d.ts
type RegisterAction = typeof import("../lib/register/action");

export interface Core {
   registerAction: RegisterAction
}

// RegisterAction.d.ts

export type RegisterAction = typeof import("../lib/register/action");

type RegisterActionOptions = Parameters<RegisterAction>[0]

export type Action = Omit<
  RegisterActionOptions,
  'name'
>

This would result in the typescript type for registerAction to be

type RegisterAction = (action: {
    name: string;
    do: Function;
    undo: Function;
}) => any

But you'll notice here that;

  • this, do and undo are no longer typed like they are in the manually constructed types
  • and that the return type has become any.

So I would not recommend it.

expectType<StyleDictionary.File>({
format: `css/variables`,
destination: `variables.css`
} as StyleDictionary.File);
Copy link
Contributor

Choose a reason for hiding this comment

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

I missed this while first reviewing, the as keyword in typescript is a type assertion. So here you are saying to typescript to force the type of that object into StyleDictionary.File, which means even if the object did not match that type, this would pass.

While TypeScript only allows type assertions which convert to a more specific or less specific version of a type ("impossible conversions" such as 1 as StyleDictionary.File), it will not prevent transforms such as {format: 'css/variables'} as StyleDictionary.File (File is typed to always require a destination).

An alternative here would be to use the expectAssignable from tsd, which will ensure that this object can be assigned to StyleDictionary.File

@dbanksdesign
Copy link
Member Author

dbanksdesign commented May 19, 2021

@luke-john I made a bunch of the updates you suggested if you want to take a look. It was all super helpful. Here is what I am proposing: if these changes look good we can merge these in for now with the expectation that we will come up with a better long-term strategy for typescript + code documentation. I think these changes are better than what style dictionary currently has, so at least it should be an improvement.

But now it looks like the build is failing. Also, when I try to use this new version locally, it messes up VSCode's JS intellisense. Switching the namespace stuff to module exports messes it up. Here is what it looks like:

Screen Shot 2021-05-19 at 1 40 47 PM

If I run that file, I can actually access all the stuff inside Style Dictionary I would expect like StyleDictionary.registerFormat, but just inellisense doesn't understand the typescript declarations in a way for a plain JS environment.

@dbanksdesign
Copy link
Member Author

Update: reverting back to the namespace way or else we lose autocomplete support in VSCode for non-typescript users. That is a bit of a non-starter. If there is a way to support CommonJS without using the namespace thing I'm all for it.

@luke-john
Copy link
Contributor

Hey, sorry for sending you down a rabbit hole with the commonjs support. Looks like the namespace + export = (export assignment) is the way to go.

While having a look into it, I noticed that the files field in package.json explicitly contains types/index.d.ts.

You'll want to either change that to something like types/**/* or manually include each of the new type files which are consumed by types/index.d.ts, otherwise they won't be available for consumers.

@dbanksdesign
Copy link
Member Author

Thanks for help! Sorry this has been a bit of a mess.

Copy link
Collaborator

@chazzmoney chazzmoney left a comment

Choose a reason for hiding this comment

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

I think this is the best compromise for now. Lets merge it. Someone who really knows TS and our documentation needs can figure it out later.

@dbanksdesign dbanksdesign merged commit db6269b into 3.0 May 22, 2021
@dbanksdesign dbanksdesign deleted the fixing-typescript branch May 22, 2021 03:44
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