かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React TypeScript Material UI のインラインスタイルで型エラーにハマる

Material ui の Modal のサンプルを TypeScript にコピペしたら JSX でインラインスタイルの指定に CSS のオブジェクトを渡している箇所で形エラーになってしまった

const style = {
  // インラインスタイルに使う CSS
};

const MyComponent = () => {
  return <div style={style} />
  // => style={style} の箇所が型エラーになる
}

環境

  • typescript@4.1.6
  • react@17.0.2
  • @mui/material@5.2.7

結論

  • 通常の JSX にCSS のオブジェクトを渡す際は React.CSSProperties の型をつける
  • Material UI の sx に渡す CSS のオブジェクトは as const するか SxProps<Theme> の型をつける

とするのが良さそう。

通常の React の JSX

const style: React.CSSProperties = {
  // インラインスタイルに使う CSS
};

const MyComponent = () => {
  return <div style={style} />
}

Material UI

Materia UI 公式のドキュメントに載っているのは as const を使う方法

import Box from '@mui/material/Box';

const style = {
  // インラインスタイルに使う CSS
} as const;

const MyComponent = () => {
  return <Box sx={style} />
}

又は

import Box from '@mui/material/Box';
import { Theme } from '@mui/material/styles';
import { SxProps } from '@mui/system';

const style: SxProps<Theme> = {
  // インラインスタイルに使う CSS
};

const MyComponent = () => {
  return <Box sx={style} />
}

cf.


経緯

tsx にインラインスタイルに CSS のオブジェクトを渡した際に型エラーになる

cf. React Modal component - MUI

// Modal.tsx
const style = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 400,
  bgcolor: 'background.paper',
  border: '2px solid #000',
  boxShadow: 24,
  p: 4,
};

export const MyModal = ():JSX.Element => {
  const [open, setOpen] = React.useState(false);
  const handleOpen = () => setOpen(true);
  const handleClose = () => setOpen(false);

  return (
    <div>
      <Button onClick={handleOpen}>Open modal</Button>
      <Modal
        open={open}
        onClose={handleClose}
        aria-labelledby="modal-modal-title"
        aria-describedby="modal-modal-description"
      >
        <Box sx={style}>
          // 略
        </Box>
      </Modal>
    </div>
  );
}

<Box sx={style}> の箇所が下記のような型エラーになる

(JSX attribute) sx?: SxProps<Theme> | undefined  
No overload matches this call. 
  Overload 1 of 2, '(props: { component: ElementType<any>; } & SystemProps<Theme> & { children?: ReactNode; component?: ElementType<any> | undefined; ref?: Ref<...> | undefined; sx?: SxProps<...> | undefined; } & CommonProps & Omit<...>): Element', gave the following error.
    Type '{ position: string; top: string; left: string; transform: string; width: number; bgcolor: string; border: string; boxShadow: number; p: number; }' is not assignable to type 'SxProps<Theme> | undefined'.
      Type '{ position: string; top: string; left: string; transform: string; width: number; bgcolor: string; border: string; boxShadow: number; p: number; }' is not assignable to type 'CSSSelectorObject<Theme>'.
        Property 'position' is incompatible with index signature.
          Type 'string' is not assignable to type 'SystemStyleObject<Theme> | ((theme: Theme) => SystemStyleObject<Theme>)'.
  Overload 2 of 2, '(props: DefaultComponentProps<BoxTypeMap<{}, "div">>): Element', gave the following error.
    Type '{ position: string; top: string; left: string; transform: string; width: number; bgcolor: string; border: string; boxShadow: number; p: number; }' is not assignable to type 'SxProps<Theme> | undefined'.ts(2769)
Box.d.ts(11, 7): The expected type comes from property 'sx' which is declared here on type 'IntrinsicAttributes & { component: ElementType<any>; } & SystemProps<Theme> & { children?: ReactNode; component?: ElementType<...> | undefined; ref?: Ref<...> | undefined; sx?: SxProps<...> | undefined; } & CommonProps & Omit<...>'
Box.d.ts(11, 7): The expected type comes from property 'sx' which is declared here on type 'IntrinsicAttributes & SystemProps<Theme> & { children?: ReactNode; component?: ElementType<any> | undefined; ref?: Ref<...> | undefined; sx?: SxProps<...> | undefined; } & CommonProps & Omit<...>'

長げぇ… (ᐡ •̥ ̫ •̥ ᐡ)

Material UI を使わないただの div タグにスタイルのオブジェクトを渡しても同じような型エラーが発生する
<div style={style} /> 👇

(JSX attribute) React.HTMLAttributes<T>.style?: React.CSSProperties | undefined
Type '{ position: string; top: string; left: string; transform: string; width: number; bgcolor: string; border: string; boxShadow: number; p: number; }' is not assignable to type 'Properties<string | number, string & {}>'.
  Types of property 'boxShadow' are incompatible.
    Type 'number' is not assignable to type 'BoxShadow | undefined'.ts(2322)
index.d.ts(1839, 9): The expected type comes from property 'style' which is declared here on type 'DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement>'

こちらの方が理由が分りやすく出ていました。要するに style 属性に渡してるオブジェクトが CSS のオブジェクトとは限らないのでエラーになっているようです。

1. JSX のインラインスタイル属性に直接スタイルを記述する

身も蓋もないけど、直接属性にスタイルを記述すると属性に合っているのかどうかで判定されるので型エラーにならない

// Modal.tsx
- const style = {
-   position: 'absolute',
-   top: '50%',
-   left: '50%',
-   transform: 'translate(-50%, -50%)',
-   width: 400,
-   bgcolor: 'background.paper',
-   border: '2px solid #000',
-   boxShadow: 24,
-   p: 4,
- };

export const MyModal = ():JSX.Element => {
  // …
  return (
    <div>
      <Button onClick={handleOpen}>Open modal</Button>
      <Modal>
-       <Box sx={style}>
+       <Box sx={{
+         position: 'absolute',
+         top: '50%',
+         left: '50%',
+         transform: 'translate(-50%, -50%)',
+         width: 400,
+         bgcolor: 'background.paper',
+         border: '2px solid #000',
+         boxShadow: 24,
+         p: 4,
+       }}>
  );
};

本当に見も蓋もないけどエラーにならないからオッケー ₍ ᐢ. ̫ .ᐢ ₎

2. CSS のオブジェクトに型定義をする

CSS の変数の型が無いので、マッチしない要素が渡される可能性があり型エラーになっているので、CSS を定義してあるオブジェクトに型を定義すれば OK

インラインスタイルで style 属性に変数でスタイルを渡す際は CSS の変数に型定義をする必要がある

<div style={style}> のような通常のJSXのインラインスタイルの場合は React.CSSProperties で型定義をすれば OK

- const style = {
+ const style: React.CSSProperties = { 
  position: 'absolute',
  top: '50%',
  border: 0,
};

export const MyComponent = (): JSX.Element => {
  return <div style={style} />
}

₍ ᐢ. ̫ .ᐢ ₎👌

Material UI は 独自のプロパティを CSS に変換するので React.CSSProperties では型エラーになってしまう

Material UI のコンポーネントは style 属性ではなく、cx という props に CSS を渡すようになっており、CSS のプロパティも {p: 4} のような独自のプロパティを指定できるので、Reacr.CSSProperties では型が合わずエラーになってしまう。

// Modal.tsx
const style: React.CSSProperties = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 400,
  bgcolor: 'background.paper', // => type error
  border: '2px solid #000',
  boxShadow: 24, // => type error
  p: 4, // => type error
};

Material UI 独特の定義方法である bgcolor: 'background.paper', boxShadow: 24, p: 4 のような箇所が Reacr.CSSProperties の型定義に合わないとエラーになる。(それはそう)

2-1. as const を使う

A frequent source of confusion with the sx prop is TypeScript's type widening, which causes this example not to work as expected:

const style = {
  flexDirection: 'column',
};
export default function App() {
  return <Button sx={style}>Example</Button>;
}
//    Type '{ flexDirection: string; }' is not assignable to type 'SxProps<Theme> | undefined'.
//    Type '{ flexDirection: string; }' is not assignable to type 'CSSSelectorObject<Theme>'.
//      Property 'flexDirection' is incompatible with index signature.
//        Type 'string' is not assignable to type 'SystemStyleObject<Theme>'.
The problem is that the type of the flexDirection prop is inferred as string, which is too wide. To fix this, you can cast the object/function passed to the sx prop to const:
cf. TypeScript usage | The sx prop - MUI

公式のドキュメントに書いてある方法。

// Modal.tsx
const style = {
  position: 'absolute',
  top: '50%',
  left: '50%',
  transform: 'translate(-50%, -50%)',
  width: 400,
  bgcolor: 'background.paper',
  border: '2px solid #000',
  boxShadow: 24,
  p: 4,
- };
+ } as const;

export const MyModal = ():JSX.Element => {
  // …
  return (
    <div>
      <Button onClick={handleOpen}>Open modal</Button>
      <Modal>
        <Box sx={style}>

₍ ᐢ. ̫ .ᐢ ₎👌
sx に渡す style のオブジェクトに as const 付ければエラーにならないんだ… というお気持ち

2-2. SxProps を使う方法

sx の型を見ていると (JSX attribute) sx?: SxProps<Theme> | undefined と定義されていました。
SxProps@mui/system から import できるので、インラインスタイルのオブジェクトにこの型を与えればうまく動作しました。

// Modal.tsx
+ import { Theme } from '@mui/material/styles';
+ import { SxProps } from '@mui/system';

- const style = {
+ const style: SxProps<Theme> = {
 // ...
};

export const MyModal = ():JSX.Element => {
  // …
  return (
    <div>
      <Button onClick={handleOpen}>Open modal</Button>
      <Modal>
        <Box sx={style}>

₍ ᐢ. ̫ .ᐢ ₎👌

公式のドキュメントには無い方法なので、正しいのかどうか判断つかず…

cf.

2-3. { [key: string]: string | number } のオレオレ型を付けても通る

SxProps の型定義の中身を深堀りしてないので、なんとも言えませんが、CSS のプロパティと値の型にできそうな { [key: string]: string | number } を使うと何故か通る。

// Modal.tsx
- const style = {
+ const style: { [key: string]: string | number } = {
  // ...
};

export const MyModal = ():JSX.Element => {
  // …
  return (
    <div>
      <Button onClick={handleOpen}>Open modal</Button>
      <Modal>
        <Box sx={style}>

🤔 通るけれども、React.CSSPropertiesSxProps<Theme> のようにプロパティとして扱えるのかまでは判定できないんど微妙だと思います。

cf. SxPropsTheme の型定義

export type SxProps<Theme extends object = {}> =
  | SystemStyleObject<Theme>
  | ((theme: Theme) => SystemStyleObject<Theme>)
  | Array<boolean | SystemStyleObject<Theme> | ((theme: Theme) => SystemStyleObject<Theme>)>;

export interface Theme extends SystemTheme {
  mixins: Mixins;
  components?: Components;
  palette: Palette;
  shadows: Shadows;
  transitions: Transitions;
  typography: Typography;
  zIndex: ZIndex;
  unstable_strictMode?: boolean;
}

所感

最初は単純に型がないなら { [key: string]: string | number } したらええやろ〜で通ってしまったので、30分くらいでメモしておくか〜と思ってエビデンスの為に少し深堀りしたら React.CSSProperties や Material UI の SxProps を知ることになって結果 LOOOOOONG な記事になってしまった。だから結論を先に書いた。

結論としては (重複するけど) 下記で良いかなと思いました。

  • 通常の JSX にわたす CSS のオブジェクトには React.CSSProperties の型を指定する
  • Material UI の sx に渡す CSS のオブジェクトは as const してしまうか、型指定したい場合は SxProps<Theme> を指定する

[参考]

そのミス9割がヒューマンエラー

やはり人間 (僕) は愚か…