Balance Logo
Balance
Reckon Design System

TypeScript Tips

When it's okay to use any and when you should avoid it

any is quite a dangerous type because it's removing all of guarantees and help that TypeScript provides. While it would be nice to say "never use any", that's just not practical because a lot of things are hard/impossible to type correctly in TypeScript so rather than outright banning it or something, we want to discourage it's usage. The general rule around any is, if it's too hard/impossible to type correctly and it's not in an API that's used elsewhere, using any is reasonably okay for that, otherwise you should try very hard to avoid it. So as an example, for the props of components and etc. you should make sure they're typed quite strictly but if you can't avoid using any in a small place in the component, that's okay.

Using as const to avoid type widening

You might have seen an error something like this:

const styles = {
textAlign: 'center',
};
// Type '{ textAlign: string; }' is not assignable to type 'InterpolationWithTheme<any>'.
// Type '{ textAlign: string; }' is not assignable to type 'ObjectInterpolation<undefined>'.
// Types of property 'textAlign' are incompatible.
// Type 'string' is not assignable to type '"start" | "end" | "right" | "left" | "center" | "justify" | "-moz-initial" | "inherit" | "initial" | "revert" | "unset" | "match-parent" | TextAlignProperty[] | undefined'.ts(2322)
<div css={styles} />;

The error message is quite big but the important thing that it's saying is Type 'string' is not assignable to type '"start" | ...'. The reason there's an error here is that the type of textAlign is restricted to certain set of string literals since there's a defined set of valid values for that CSS property and TypeScript sees that the type of the textAlign property is string in styles and string is not assignable to any string literal.

You might be thinking "but I wrote "center" in the object, why is it erroring?" What's going on here is when we define styles, TypeScript is inferring the type of the object as { textAlign: string; }, it's "widening" the type of "center" to string because it's trying to avoid the alternative of people going "why can't I assign a different string to this thing?"

In this case though, we don't want TypeScript to widen the type to string, we want it to be inferred as "center". The easiest way to make TypeScript not widen the type is to use as const. You might have seen casting in TypeScript with as, as const is like as except rather than casting to a given type, it's taking the type and making it the literal or readonly form, concretely, this means:

  • string literals will be inferred as string literals rather than string
  • numeric literals will be inferred as numeric literals rather than number
  • properties on object literals will be readonly
  • array literals will be inferred as readonly tuples

So we can add as const after the object and the error will go away.

const styles = {
textAlign: 'center',
} as const;
<div css={styles} />;

Typing the props of DOM elements

If you want to create a create a component that spreads props to a dom element, you can use the various *HTMLAttributes types from @types/react. There are various types for different elements, use your editor's autocomplete to find the right one.

import { InputHTMLAttributes } from 'react';
type MyComponentProps = {
something: string;
} & InputHTMLAttributes<HTMLInputElement>;
const MyComponent = ({ something, ...props }: MyComponentProps) => {
return <input {...props} />;
};

Some elements like div, section and etc. don't have specific types, you should use HTMLAttributes for these.

import { HTMLAttributes } from 'react';
type MyComponentProps = {
something: string;
} & HTMLAttributes<HTMLElement>;
const MyComponent = ({ something, ...props }: MyComponentProps) => {
return <div {...props} />;
};

If you've got props that conflict with the DOM element props, you can use Omit to remove the conflicting props from the type of the DOM element props before you intersect it with your own props.

import { InputHTMLAttributes } from 'react';
type MyComponentProps = {
value: {
something: boolean;
};
onChange: (value: boolean) => void;
} & Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'>;
const MyComponent = ({ value, ...props }: MyComponentProps) => {
return <input {...props} />;
};

Array.prototype.filter and type predicates

If you've ever filtered some items out of an array in TypeScript, you've probably seen an error like this because TypeScript didn't understand that your filtering removed certain items.

const acceptsArrayOfStrings = (arr: string[]) => {};
const myStuff = ['something', 'another', null, undefined];
const myStuffWithoutNullishValues = myStuff.filter((item) => item != null);
// Argument of type '(string | null | undefined)[]' is not assignable to parameter of type 'string[]'.
// Type 'string | null | undefined' is not assignable to type 'string'.
// Type 'undefined' is not assignable to type 'string'.ts(2345)
acceptsArrayOfStrings(myStuffWithoutNullishValues);

To solve this, you can make the return value of the function a type predicate.

const acceptsArrayOfStrings = (arr: string[]) => {};
const myStuff = ['something', 'another', null, undefined];
const myStuffWithoutNull = myStuff.filter(
(item): item is string => item != null
);
acceptsArrayOfStrings(myStuffWithoutNullishValues);
Copyright © 2021 Reckon. Designed and developed in partnership with Thinkmill.