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

Remove all ts-ignore in vue, tighten typings #11547

Closed
wants to merge 2 commits into from

Conversation

abrenneke
Copy link

What I did

Well while learning how the Vue integration works for fixing unrelated issues I went down the typescript rabbit-hole and ended up improving the typings for Vue, and a little bit for all of the apps.

  • All @ts-ignore and most todo are removed
  • Reduced the amount of any in the code
  • Tightened the typings, for example StoryFnVueReturnType is now string | VueConstructor | VueOptions, where VueOptions is any argument that Vue.extend() accepts.
  • More edge cases revealed and accounted for, because of the more-correct typings.

Some of the changes (such as to @storybook/client-api and @storybook/core) leaked out of the changes here and into the other apps. Those changes enhance type-safety across the board, so in my opinion they're good to have. I'll add comments to point them out.

@backbone87 reviewed #7578 and I got a lot of insight from those comments so maybe they would want to look this over.

All tests, builds, & linting passing (on my machine at least).

How to test

  • Is this testable with Jest or Chromatic screenshots?
    Existing tests cover the changes
  • Does this need a new example in the kitchen sink apps?
    Nope
  • Does this need an update to the documentation?
    Nope

@@ -3,7 +3,6 @@
"compilerOptions": {
"rootDir": "./src"
},
"include": ["src/**/*", "typings.d.ts"],
"exclude": ["src/**.test.ts"],
"files": ["./typings.d.ts"]
Copy link
Author

@abrenneke abrenneke Jul 15, 2020

Choose a reason for hiding this comment

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

Seemed redundant since typings.d.ts was included with include already.

@@ -0,0 +1,2 @@
import start from './dist/client/index';
export = start;
Copy link
Author

@abrenneke abrenneke Jul 15, 2020

Choose a reason for hiding this comment

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

Adding typings to client.js here caused all the apps to go from import { start } from '@storybook/core/client' to import client from '@storybook/core/client'.

However, the actual implementation of core/client.js, core/src/index.js, and core/src/preview/index.ts matches this, as core/src/client/preview/index.ts exports an object as default instead of exporting multiple functions. Therefore to match that, has to be default-imported in typescript.

core/src/client/preview/index.ts could be changed to a more correct module, but I'm not sure if there was a reason it was done the way it is right now.

Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure to understand why import { start } from '@storybook/core/client' need to become import client from '@storybook/core/client'. I added locally this lib/core/client.d.ts and has no error with import { start } from '@storybook/core/client'.

Should start not be client instead as start is only a property of the exported object?

Copy link
Author

@abrenneke abrenneke Jul 16, 2020

Choose a reason for hiding this comment

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

That's strange that it worked fine for you @gaetanmaisse - typescript was definitely erroring for me when I kept it as import { start }.

You're right that it works fine once it compiles down to CommonJS, as it's basically const { start } = require('@storybook/client-api') either way, but it's not valid in ES modules or typescript to import a single property of a default-exported object. Babel fudges this restriction a bit, when interfacing with CommonJS code.

For correct ES modules, we would need to have:

core/src/client/preview/index.ts

export { ClientApi, StoryStore, ConfigApi } from '@storybook/client-api';
export { toId } from '@storybook/csf';
export { default as start } from './start';

core/src/client/index.ts

export * from './preview';
export * from './preview/types';

core/client.d.ts

export * from './dist/client';

Which is definitely simpler, but I didn't know if there was a reason for the current way, so I didn't touch that.

Copy link
Member

Choose a reason for hiding this comment

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

👍 thanks for the clarification, I agree with you it will be way simpler/clearer with the solution in your comment. I had the same question about the reason of this kind of export in other parts of Storybook but I never had a strong answer, so it's surely a code style legacy.

Copy link
Author

Choose a reason for hiding this comment

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

I'll go ahead and switch to the new style and hope nothing breaks 😄

Copy link
Author

Choose a reason for hiding this comment

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

This is really bizzare, yarn build core works, yarn build react works, yarn build core api react vue works, but yarn build --all fails when building @storybook/react. Is there anything funky going on when I run --all @gaetanmaisse?

Copy link
Member

Choose a reason for hiding this comment

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

Hmm I would say no, but maybe 🙈 😂 Is it working with yarn bootstrap --core?

@@ -95,7 +95,7 @@ export interface GetStorybookKind {

// This really belongs in lib/core, but that depends on lib/ui which (dev) depends on app/react
// which needs this type. So we put it here to avoid the circular dependency problem.
export type RenderContext = StoreItem & {
export type RenderContext<StoryFnReturnType = unknown> = StoreItem<StoryFnReturnType> & {
Copy link
Author

Choose a reason for hiding this comment

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

This required some changes in the other apps besides Vue, but now they all get the correct type from calling storyFn() instead of any, so it's a win in my opinion. It's technically breaking if there are any other apps out there, that expect any instead of unknown? unknown is generally more type-safe though.

"resolveJsonModule": true
"resolveJsonModule": true,
"strict": true,
"noImplicitReturns": true
Copy link
Author

Choose a reason for hiding this comment

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

Made the Vue app as strict as I could for typescript, to catch everything, can revert these if wanted.

{
files: ['*.ts', '*.tsx'],
rules: {
'@typescript-eslint/explicit-function-return-type': 'error',
Copy link
Author

Choose a reason for hiding this comment

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

Made the Vue app as strict as I could for typescript, to catch everything, can revert these if wanted.

// known limitation: we don't have the component instance to pass
return def.call();
return (validator.default as () => T | null | undefined)();
Copy link
Author

Choose a reason for hiding this comment

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

Tried as I might, I couldn't get this to be completely type-safe using a type guard. I think this would need negated types to remove T & Function from the possible types.

// TODO: some vue expert needs to look at this
export type StoryFnVueReturnType = string | Component;
/** Any options that can be passed into Vue.extend */
export type VueOptions = Exclude<Parameters<VueConstructor['extend']>[0], undefined>;
Copy link
Author

Choose a reason for hiding this comment

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

I could not find a union of all options interfaces in the Vue typings, pulling all possible arguments to extend gets them all quite easily. The types are very long otherwise. Let me know if you know a better type.

@@ -5,7 +5,6 @@ export {
addParameters,
configure,
getStorybook,
forceReRender,
Copy link
Author

Choose a reason for hiding this comment

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

I'm guessing this was an old field - not present any more once type-safety was added.

const element = storyFn();

if (!element) {
showError({
title: `Expecting an Aurelia component from the story: "${selectedStory}" of "${selectedKind}".`,
title: `Expecting an Aurelia component from the story.`,
Copy link
Author

Choose a reason for hiding this comment

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

Newer way to get the story name? I didn't look too far into it.

@abrenneke abrenneke force-pushed the vue-typescript-safety branch from a985e88 to 64373e2 Compare July 15, 2020 01:27
package.json Outdated
@@ -240,7 +240,7 @@
"ts-dedent": "^1.1.1",
"ts-jest": "^26.1.0",
"ts-node": "^8.9.1",
"typescript": "^3.4.0",
"typescript": "^3.7.0",
Copy link
Author

Choose a reason for hiding this comment

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

Typescript 3.9 is already pinned in the yarn.lock, but since I used optional chaining which requires at least typescript 3.7, I figured this should be changed

Copy link
Member

Choose a reason for hiding this comment

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

I think you can set ^3.9.6 directly because as you said we are already using it

Copy link
Author

Choose a reason for hiding this comment

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

Fair enough!

@gaetanmaisse it looks like there are a lot of duplicates in yarn.lock (like, typescript shows up 15 times in yarn why typescript), it might be a good idea to run yarn-deduplicate to clean that up.

Copy link
Member

Choose a reason for hiding this comment

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

True! I plan to do a big dep clean and dedup work in Storybook 6.1 :)


function prepare(rawStory: undefined, innerStory?: VueConstructor): undefined;
Copy link
Author

Choose a reason for hiding this comment

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

A little bit awkward, but maintains the type-safety effectively, since prepare will only return undefined if rawStory === undefined, which won't happen if someone uses typescript, but may happen otherwise.

@@ -28,7 +28,7 @@ const setRenderFetchAndConfigure: ClientApi['configure'] = (loader, module, opti
if (options && options.fetchStoryHtml) {
setFetchStoryHtml(options.fetchStoryHtml);
}
api.configure(loader, module, framework);
api.configure(framework, loader, module);
Copy link
Author

Choose a reason for hiding this comment

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

This appeared to be just wrong? Was revealed by the type-safety

@shilman
Copy link
Member

shilman commented Jul 15, 2020

@abrenneke This looks amazing, great work!!! @backbone87 can you please take a look?

Less any, more explicit types and type checks
Turned on noImplicitReturns, strict, explicit-function-return-type (vue)
@abrenneke abrenneke force-pushed the vue-typescript-safety branch from 64373e2 to 49f4a3a Compare July 15, 2020 01:39
@abrenneke abrenneke requested a review from gaetanmaisse July 17, 2020 19:48
@gaetanmaisse
Copy link
Member

@abrenneke I will take a second look asap 😉

@shilman shilman added this to the 6.1 core milestone Jul 31, 2020
@shilman shilman modified the milestones: 6.1 core, 6.2 core Nov 4, 2020
@abrenneke abrenneke closed this Feb 10, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants