かもメモ

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

TypeScript interface のプロパティを引数に取る関数を定義したい

例えば次ようなサインアップフォームのフィールドの値を引数に取るバリデーションを行う関数あるとして、この関数の型を別途定義したい

interface SignUp {
  username: string;
  email: string;
  password: string;
}

const validation = ({ username, email, password }: Partial<SignUp>): boolean = {...}
// => この関数の型を別途定義したい

引数が interface のオブジェクトのままなら [key in keyof Interface] を使って型定義できる

interface SignUp {
  username: string;
  email: string;
  password: string;
}

type ValidationFuncType = (arg: Partial<SignUp>) => boolean;

const validation: ValidationFuncType = ({ username, email, password }) => {}

オブジェクト展開して関数で受け取っているので、arg: Partial<[key in keyof SignUp]> の様な定義ができる。関数の型定義のとろこにオブジェクトを分割した際の引数を書かなくても関数の引数で保管が効くようになりる。 type F = (Partial<[key in keyof SignUp]>) => boolean のうような書き方はできないっぽい。

validation 関数が受け取る値が new FormData で取得する値なら次のように定義できる

type ValidationFuncType = (arg: Partial<{
  [key in keyof SignUp] : FormDataEntryValue | null;
}>) => boolean;

受け取るプロパティを追加したい場合は & (intersection) で引数のオブジェクトのプロパティを追加してしまえばOK

type ValidationFuncProps = Partial<{
  [key in keyof SignUp] : FormDataEntryValue | null;
}> & {
  isSignUp: boolean;
};

type ValidationFuncType = (arg: ValidationFuncProps) => boolean;

// isSignUp の型補完も効くようになる!
const validation: ValidationFuncType = ({ username, email, password, isSignUp }) => {}

渡さないプロパティがある場合は Omit で取り除けば良さそう

もっと良い定義方法があれば教えて下さい!


[参考]

React Material UI テキストフィールドを readonly にしたい

Material UI の <TextField /> コンポーネントは出力時にいい感じに div で囲ってくれるからか、直接 readonly 属性を渡しても設定されなかった。

  • @mui/material@5.2.7

NG: <TextField /> に readonly 属性を渡しても readonly な状態にはならない

disabled は効くけど readonly は効かない

import { TextField } from '@mui/material';

const MyComponent = (): JSX.Element => {
  return  (
    <>
      <TextField label="disabled" disabled /> {/* disabled になる */}
      <TextField label="readonly" readonly /> {/* readonly にはならない */}
    </>
  );
}

inputProps を使えば OK

inputProps 属性に input タグに渡したい属性を props として渡せるっぽいので、この props 経由で readonly を指定できる

import { TextField } from '@mui/material';

const MyComponent = (): JSX.Element => {
  return  (
    <>
      <TextField label="disabled" disabled /> {/* disabled になる */}
      <TextField label="readonly" inputProps={{ readonly: true }} /> {/* readonly になる */}
    </>
  );
}

<Input /> コンポーネントには直接 readonly を指定できる

<TextField /> コンポーネントは HelpText とか色々とよしなに出力してくれますが、デザイン的に問題がないならシンプルに input を出力する <Input /> コンポーネントを使えば簡単です。

import { Input } from '@mui/material';

const MyComponent = (): JSX.Element => {
  return  (
    <>
      <Input label="disabled" disabled /> {/* disabled になる */}
      <Input label="readonly" readonly /> {/* readonly になる */}
    </>
  );
}

ドキュメントには <FormControl /> で囲って FormControl に disabled を設定して子の <Input /> を disabled にする方法が載ってるけど、readonly も disabled も直接 <Input /> コンポーネントに指定することができました。

cf. Inputs | Text Field React component - MUI

 
今回は単純にライブラリの使い方のメモでした。
おわり


[参考]

かわよい

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割がヒューマンエラー

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