Solving Typescript "Problems" in React: How to Argue with Typescript and Win, Even When You're Wrong

 

When a dev first adds typescript to a project, they often complain that it slows them down. Why install something whose only job is to complain about perfectly good software?

Typescript, like a linter and an IDE, can help you catch mistakes before you break production or solicit annoyed comments from senior devs.

So I'm going to give you some advice below that's... controversial. But I believe in dipping your toe into the water before diving into the deep end. When you're first starting out (and your team says so), it's okay to just get Typescript to stop complaining, get the syntax right, get a feel for it, and then do it the "right" way. There are some bad practices below, but I'm showing them to you so you can take shortcuts where needed.

Start with strict mode false

Here's my first piece of advice. When you're first starting with Typescript, turn off strict mode until you're nearly done. Then, after you've built your feature, turn on strict mode and clean up the errors.

In tsconfig.json"

{
  "compilerOptions": {
    ...other stuff
    "strict": false,
  }
}

Starting with strict mode as false will mean Typescript will complain about some things, but not all the things. That will keep you coding at a decent pace, solving a problem here and there, and then at the end, once you've learned a thing or two about Typescript, you can turn it all the way on.

Also... just turn off strict mode for your tests. In my opinion, tests and Typescript play similar roles for a project. Guardrails for your guardrails just slows down tests, which are often rushed anyways. I'm against anything that makes testing harder.

You can turn off Typsecript for jest inside jest.tsconfig.js. But you'll need to declare that file in jest.config.js:

{
 globals: {
    tsConfig: 'jest.tsconfig.json',
  },
}

Make a shared Typescript definitions file

Start with a types.ts in the root of your project. Move any definitions that you require elsewhere in that file. Later, if it gets full, break it into separate files as make sense.

When you put a definition in that file, don't account for `null` or empty objects. Just put them in clean like so:

export interface Params {
  [key: string]?: string | number | boolean;
  myImportantValue: string;
}
 
const fetchData = (params: Params | {} = {}): : Promise<DataItem[]> => //query stuff here;

The above is a definition for axios query parameters. It's assuming that sometimes I might want to pass in an empty object, and it will default to an empty object if no parameter is provided. It allows any key/value pair (for better or for worse). But the interface definition specifies that myImportantValue is required. It's when I add Params | {} = {} in the function definition that I allow for the empty object.

I'm saying in the function definition that the parameter I receive might be an empty object, not in the Typescript interface.

But I could account for this value as empty in the as a Typescript type (but I don't want to, and I'll explain why):

type Params = {
  [key: string]?: string | number | boolean;
  myImportantValue: string;
} | {};
 
const fetchData = (params: Params = {}): : Promise<DataItem[]> => //query stuff here;

The above is functionally equivalent to the first example, isn't it?

So why do I not want to do this?

Because later, in either of these cases, if I access params.myImportantValue, Typescript will complain.

But if I need to access that value (knowing that it's not an empty object), I'd perform a type assertion: (params as Params).myImportantValue.

Now, I can assert to the compiler that I know better than it does, that params is actually of the Params type, which means that for sure has myImportantValue.

Dealing with HOCs and props.children

Below is a very simple component that wraps any children passed to it in a link, but safely, avoiding the eslint rule react/jsx-no-target-blank:

import React from 'react';
 
interface WithLinkProps {
  href: string;
  children: any;
  [key: string]: any;
}
 
const WithLink: React.FunctionComponent<WithLinkProps> = ({
  href,
  children,
  ...restProps
}: WithLinkProps): JSX.Element => {
  return href ? (
    <a {...restProps} href={href} rel="noreferrer noopener">
      {children}
    </a>
  ) : (
    <>{children}</>
  );
};
 
export default WithLink;

This would be used like so: <WithLink href="http://test.com"><img src="test.jpg"><WithLink>

The problems I solve here:
* restProps: I don't want to consider what props are being thrown at this component. I just what is basically a link tag, and I want to pass all extra pros to the a tag with the rest parameter and spread the attributes. That's why I have [key: string]: any; in WithLinkProps. Is this true to Typescript? No. It's a bit of a caveat emptor pattern. So it's not great for large codebases with low trust, and some consider spread attributes an antipattern.
* children: The React children element can be a string, a JSX Element, an array of JSX Elements, or a function... among other things. Don't be afraid to use children: any;
* The return type: I've set the return type to be JSX.Element, and I'm ensuring that this is the case (rather than returning a veritable menagerie as described above) by wrapping my return in a JSX fragment.

Conclusion

The whole point of Typescript is to respect its complaints. So when you find yourself being ham-handed with it, you're likely doing it wrong. So it's good to be reflective instead of reaching for a sledgehammer.

But sometimes you do need to assert that you know better than Typescript, and I hope this gallimaufry helps others.

About the Author

Hi. My name is Jeremiah John. I'm a sf/f writer and activist.

I just completed a dystopian science fiction novel. I run a website which I created that connects farms with churches, mosques, and synagogues to buy fresh vegetables directly and distribute them on a sliding scale to those in need.

In 2003, I spent six months in prison for civil disobedience while working to close the School of the Americas, converting to Christianity, as one does, while I was in the clink.