Skip to content

Instantly share code, notes, and snippets.

@slackday
Last active May 15, 2020 10:57
Show Gist options
  • Save slackday/7142251cf2bb3660f8c24492aa1f700d to your computer and use it in GitHub Desktop.
Save slackday/7142251cf2bb3660f8c24492aa1f700d to your computer and use it in GitHub Desktop.
Typescript/styled-components/no-explicit-any question
// Project 1:
// Styled componet wrapper might look like this
const styledFactory = (
target: any,
displayName: string,
...systemFunctions: styleFn[]
): any => {
const Component = styled(target)`
min-width: 0;
${sx}
${compose(animation, color, flexbox, layout, space, ...systemFunctions)}
`;
Component.displayName = displayName;
return Component;
};
//-----
// Project 2 tries to use same function but with Typescript
// But with typescript lint rule "no-explicit-any"
// Disallow usage of the any type
// https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md
// Same component becomes wild goose chase of finding the right types for dependencies
const styledFactory = (
target: AnyStyledComponent,
displayName: string,
...systemFunctions: ThemedStyledFunction[]
): AnyStyledComponent => {
const Component = styled(target)`
min-width: 0;
${sx}
${compose(animation, color, flexbox, layout, space, ...systemFunctions)}
`;
Component.displayName = displayName;
return Component;
};
@m-rutter
Copy link

m-rutter commented May 13, 2020

Its not clear to me why you would want this function. One reason why you are having trouble is that ThemedStyledFunction requires 4 generic type parameters with only two of them are set with defaults. I don't think this interface was designed for common usage outside of authoring a library that extends styled-component's existing behaviour.

Is there a reason why you need a factory function when styled component instances are typically defined in module scope at initialisation?

@slackday
Copy link
Author

I took this example as it was one of the most recent ones I could think of. One project uses React with styled-components and styled-system https://github.com/styled-system/styled-system

Function works it applies some styling and a displayName to component.

Second project is everything from first project and using typescript. When copying the function to second project linter and ts-compiler might say incorrect return type. So I must try and adjust the function to fix this and this is were one must figure out the return types of underlying library.

Any good way of doing this? It might depend heavily on the case but sometimes just disabling the rules feels tempting but that would defeat the purpose of ts?

Or one might think to disable the rules for every dependency and just keep ts for application code? I'm not sure and still learning what is best.

@slackday
Copy link
Author

Ok another example I'm just working on.

I want to create a hook for a menu to close when the user clicks outside the parent element.

Looks like this

import { useEffect } from 'react';

const useOutsideClick = (ref, callback) => {
  const handleClick = e => {
    if (ref.current && !ref.current.contains(e.target)) {
      callback();
    }
  };

  useEffect(() => {
    document.addEventListener('click', handleClick);

    return () => {
      document.removeEventListener('click', handleClick);
    };
  });
};

export default useOutsideClick;

Here I also get stuck on figuring out the types. For ref and callback it can be straight forward. But then I get to the event.

React.mouseEvent makes sense. But document.addEventListener has no overload for this. "No overload matches this call".

Maybe just MouseEvent... nope.

Then I remember React uses synthetic events, and my brain starts to hurt 🤯, so maybe React.SyntheticEvent hmm no.

Is there any easier way? Or should I just learn the whole documentation and all return types?

import { useEffect } from 'react';

const useOutsideClick = (
  ref: React.RefObject<HTMLInputElement>,
  callback: () => void,
) => {
  const handleClick = (evn: React.MouseEvent<HTMLButtonElement>) => {
    if (ref.current && !ref.current.contains(evn.target)) {
      callback();
    }
  };

  useEffect(() => {
    document.addEventListener('click', handleClick);

    return () => {
      document.removeEventListener('click', handleClick);
    };
  });
};

export default useOutsideClick;

Just looking for tips. Thanks!

@WorldMaker
Copy link

React HOCs are tough to type. One theoretical benefit to Hooks is hopefully less need for HOCs like that styled-component factory.

One thing I find useful to note is that no-implicit-any doesn't stop you from using an explicit any. Don't fear using any as an escape valve just because no-implicit-any even when following the good advice to no-implicit-any, just do it explicitly (and then you can search for any as a TODO list when you do find or need to find a better type) and/or include a TODO comment next to it as well.

For instance:

const styledFactory = (
  target: AnyStyledComponent,
  displayName: string,
  ...systemFunctions: ThemedStyledFunction[]
): any /* TODO: explicit any because I don't have time to figure this out */ => {
    const Component = styled(target)`
      min-width: 0;
      ${sx}
      ${compose(animation, color, flexbox, layout, space, ...systemFunctions)}
    `;
    Component.displayName = displayName;
    return Component;
  };

As for your second example, remember that because you are calling window.document you are escaping out of the React world back to the old world DOM. In that case you aren't using one of React's synthetic events, but an old fashioned DOM event and it is just called MouseEvent (not React.MouseEvent). In that case, you can use Typescript's own power to help you find that out. If you hover over the addEventListener('click', function name in most IDEs you should see that type information it expects for its callback argument. (Something neat there is that Typescript is smart enough to know based on that first string argument exactly which type.)

@WorldMaker
Copy link

A more advanced trick for the HOC case is the ReturnType<T> generic option, where you use Typescript's inferencing engine directly in finding out what the return type of something else is.

const styledFactory = (
  target: AnyStyledComponent,
  displayName: string,
  ...systemFunctions: ThemedStyledFunction[]
): ReturnType<typeof styled> => {
    const Component = styled(target)`
      min-width: 0;
      ${sx}
      ${compose(animation, color, flexbox, layout, space, ...systemFunctions)}
    `;
    Component.displayName = displayName;
    return Component;
  };

Because you are simply returning what styled() itself returns, you can ask Typescript for the ReturnType of (the typeof) that function.

@slackday
Copy link
Author

Hey Max thanks a lot for taking your time providing me with thorough examples. It's all starting to make much more sense to me. Not to get stuck on lint errors so it's a good idea to document Todo and come back to it later.

I really like the generic return type option. Thinking about it. This probably solves 9/10 issues I have had in the past. Often the use case is just to return whatever the underlying method returns (in the dependency). And this approach seems like the appropriate way of handling it in a clean way 💯 👍

I also got the tips on hackernews to look into *.d.ts files which I was unaware of. Again thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment