跳转到内容

🎉 Material UI v5 is out now! Check out the announcement blog post →

TypeScript

借助 TypeScript,你可以为 JavaScript 添加静态类型,从而提高代码质量及开发者的工作效率。

Material-UI 需要最低 TypeScript 的版本为 3.2。

请查看一下 Create React App with TypeScript 的例子。

若想使用类型,你必须在 tsconfig.json 里启用以下的一些选项:

{
  "compilerOptions": {
    "lib": ["es6", "dom"],
    "noImplicitAny": true,
    "noImplicitThis": true,
    "strictNullChecks": true
  }
}

对每个发布在 @types/ 命名空间下的类型声明包,同样需要启用严格模式(strict mode)。 使用不太严格的 tsconfig.json 或省略某些库可能会造成一些错误。 若您想获得最佳类型(type)的体验,我们建议设置 "strict": true 。

withStyles 的使用

在 TypeScript 中使用 withStyles 可能有点棘手,但有一些工具集可以使体验尽可能地轻松。

使用 createStyles 来杜绝类型扩展

造成混淆的一个常见原因是 TypeScript的 类型扩展(type widening),这会导致以下示例无法按预期工作:

const styles = {
  root: {
    display: 'flex',
    flexDirection: 'column',
  }
};

withStyles(styles);
//         ^^^^^^
//        属性 “flexDirection”` 的类型是不兼容的。
//           “string” 类型不能赋予给这些类型:'"-moz-initial" | "inherit" | "initial" | "revert" | "unset" | "column" | "column-reverse" | "row"...'。

问题是 flexDirection 属性的类型被推断为 string,这太过随意。 要解决此问题,您可以将样式对象直接传递给 withStyles:

withStyles({
  root: {
    display: 'flex',
    flexDirection: 'column',
  },
});

然而,如果您尝试根据主题来构建样式,那么类型扩展会再次显示其不怎么雅观的部分:

withStyles(({ palette, spacing }) => ({
  root: {
    display: 'flex',
    flexDirection: 'column',
    padding: spacing.unit,
    backgroundColor: palette.background.default,
    color: palette.primary.main,
  },
}));

这是因为 TypeScript 扩展了函数表达式的返回类型。

因此,我们建议使用我们的 createStyles 帮助函数来构造样式规则对象:

// 不依赖于主题的样式
const styles = createStyles({
  root: {
    display: 'flex',
    flexDirection: 'column',
  },
});

// 依赖于主题的样式
const styles = ({ palette, spacing }: Theme) => createStyles({
  root: {
    display: 'flex',
    flexDirection: 'column',
    padding: spacing.unit,
    backgroundColor: palette.background.default,
    color: palette.primary.main,
  },
});

createStyles 只是一个恒等函数(identity function);它不会在运行时“做任何事情”,只是在编译时帮助指导类型推断。

Media queries(媒体查询)

withStyles 允许样式对象具有顶级媒体查询的权限,如下所示:

const styles = createStyles({
  root: {
    minHeight: '100vh',
  },
  '@media (min-width: 960px)': {
    root: {
      display: 'flex',
    },
  },
});

但是,为了允许这些样式来传递 TypeScript,鉴于CSS 类的名称和实际的 CSS 属性名称不一致,所赋予的定义必须是模糊的。 由于类名称应与 CSS 属性相同,因此应避免使用。

// 这样是错误的,由于 TypeScript 认为 `@media (min-width: 960px)` 是一个类名
// 并且 `content` 是 css 属性
const ambiguousStyles = createStyles({
  content: {
    minHeight: '100vh',
  },
  '@media (min-width: 960px)': {
    content: {
      display: 'flex',
    },
  },
});

// 这样定义即可
const ambiguousStyles = createStyles({
  contentClass: {
    minHeight: '100vh',
  },
  '@media (min-width: 960px)': {
    contentClass: {
      display: 'flex',
    },
  },
});

使用 WithStyles 来扩充你的属性

由于用 withStyles(styles) 装饰的组件被注入了一个特殊的 classes 属性,您需要相应地定义其属性:

const styles = (theme: Theme) => createStyles({
  root: { /* ... */ },
  paper: { /* ... */ },
  button: { /* ... */ },
});

interface Props {
  // 非样式的属性
  foo: number;
  bar: boolean;
  // 注入的样式的属性
  classes: {
    root: string;
    paper: string;
    button: string;
  };
}

然而,这是不是很 DRY ,因为它需要你在两个不同的地方保持类名(如 “root”, “paper”, “button”,...)。 我们提供了一个类型操作符 WithStyles 来帮助解决这个问题,因此您可以直接写入::

import { WithStyles, createStyles } from '@material-ui/core';

const styles = (theme: Theme) => createStyles({
  root: { /* ... */ },
  paper: { /* ... */ },
  button: { /* ... */ },
});

interface Props extends WithStyles<typeof styles> {
  foo: number;
  bar: boolean;
}

装饰组件

将 withStyles(styles) 作为函数来如期使用:

const DecoratedSFC = withStyles(styles)(({ text, type, color, classes }: Props) => (
  <Typography variant={type} color={color} classes={classes}>
    {text}
  </Typography>
));

const DecoratedClass = withStyles(styles)(
  class extends React.Component<Props> {
    render() {
      const { text, type, color, classes } = this.props
      return (
        <Typography variant={type} color={color} classes={classes}>
          {text}
        </Typography>
      );
    }
  }
);

不幸的是,由于 TypeScript 装饰器现有的限制, 在 TypeScript 中,withStyles(styles)作为一个装饰器使用。

定制的主题

将自定义属性添加到 Theme 中时,您可以通过以强类型的方式实现 TypeScript 的模块扩充 而继续使用它 。

以下示例添加了一个 appDrawer 属性,并将其合并到由 material-ui 提供的属性中:

import { Theme } from '@material-ui/core/styles/createTheme';
import { Breakpoint } from '@material-ui/core/styles/createBreakpoints';

declare module '@material-ui/core/styles/createTheme' {
  interface Theme {
    appDrawer: {
      width: React.CSSProperties['width']
      breakpoint: Breakpoint
    }
  }
  // 允许用 `createTheme` 来配置
  interface ThemeOptions {
    appDrawer?: {
      width?: React.CSSProperties['width']
      breakpoint?: Breakpoint
    }
  }
}

以及一个带有额外的默认选项的一个自定义主题仓库:

./styles/createMyTheme:

import { createTheme, ThemeOptions } from '@material-ui/core/styles';

export default function createMyTheme(options: ThemeOptions) {
  return createTheme({
    appDrawer: {
      width: 225,
      breakpoint: 'lg',
    },
    ...options,
  })
}

也可以这样使用:

import createMyTheme from './styles/createMyTheme';

const theme = createMyTheme({ appDrawer: { breakpoint: 'md' }});

component 属性的用法

你可以通过 component 属性替换许多 Material-UI 的许多组件的根节点,我们在组件的 API 文档中做了详细的说明。 例如,一个按钮(Button)的根节点可以被替换成一个 React Router 的链接(Link),并且,任何传入按钮(Button)的额外的属性,例如 to ,都会被传递到链接(Link)组件中。 关于按钮组件和 react-router-dom 的代码示例查看 这些示例。

为了能够单独使用 Material-UI 组件的属性,该属性应该与类型参数一起使用。 否则,component 属性将不会出现在 Material-UI 组件的属性中。

下面的示例使用了 TypographyProps,这也同样适用于那些带有 OverrideProps 定义的属性的组件。

以下 CustomComponent 组件与 Typography 组件具有相同的属性。

function CustomComponent(props: TypographyProps<'a', { component: 'a' }>) {
  /* ... */
}

按照以上示例来设置,现在的 CustomComponent 就可以与 component 属性一起使用了,并且该属性应该设置为 'a'。 此外,CustomComponent 将拥有 <a> 这个 HTML 元素的所有属性。 Typography 组件的其他属性也会出现在 CustomComponent 的属性中。

而通过一个通用的 CustomComponent 来接受任何React组件、自定义的和 HTML 元素也是有可能的。

function GenericCustomComponent<C extends React.ElementType>(
  props: TypographyProps<C, { component?: C }>,
) {
  /* ... */
}

现在,如果将 GenericCustomComponent 与所提供的 component 属性一起使用,它也应该拥有所提供的组件所需的所有属性。

function ThirdPartyComponent({ prop1 } : { prop1: string }) {
  return <div />
}
// ...
<GenericCustomComponent component={ThirdPartyComponent} prop1="some value" />;

当所需的 ThirdPartyComponent 是明确要求时,prop1 也成为 GenericCustomComponent 的必需属性。

但是,并不是每个组件都完全支持您传入的任何组件类型。 如果您在 TypeScript 中遇到一个不接受其 component 属性的组件,请新建一个 issue。 我们也一直在努力实现组件属性的通用化。

处理 值(value) 和事件处理器

很多与用户输入有关的组件会提供一个 value 属性或者包含当前 值 的事件处理器。 大多数情况下只在 React 内部处理值,这样的话它能够是任何类型,譬如 objects 或者 arrays。

然而,如果是它依赖于组件子项的情况,此类型无法在编译时被验证,例如对于 Select 或者 RadioGroup 来说。 这意味着留给我们的最合适的选项是将其输入为 unknown 并让开发者自行决定如何来缩小该类型。 与 event.target 在 React 中并不通用的原因 相同,我们并不推荐您在这些案例中尝试使用一个通用的类型。

这些演示包括使用类型转换的类型变量。 鉴于所有的类型都位于一个文件中,并且都是非常基本的,这样的折衷可以接受。 您必须自行决定是否能够接受同样的折衷。 库的类型默认为严格的,但也可以通过选择性加入变得通融一点。