跳至主要内容

按用例分类的有用模式

包装/镜像

包装/镜像 HTML 元素

用例:你想创建一个 `<Button>` 组件,它接收 `<button>` 的所有普通 props 并执行额外操作。

策略:扩展 `React.ComponentPropsWithoutRef<'button'>`

// usage
function App() {
// Type '"foo"' is not assignable to type '"button" | "submit" | "reset" | undefined'.(2322)
// return <Button type="foo"> sldkj </Button>

// no error
return <Button type="button"> text </Button>;
}

// implementation
export interface ButtonProps extends React.ComponentPropsWithoutRef<"button"> {
specialProp?: string;
}
export function Button(props: ButtonProps) {
const { specialProp, ...rest } = props;
// do something with specialProp
return <button {...rest} />;
}

在 TS Playground 中查看

转发 Refs:大多数用例不需要获取内部元素的 ref。但是,在构建可重用组件库时,通常需要 `forwardRef` 将内部组件的基础 DOM 节点暴露给父组件。然后,您可以使用 `ComponentPropsWithRef` 获取包装组件的 props。有关更多信息,请查看 我们关于转发 Refs 的文档

为什么不使用 `ComponentProps` 或 `IntrinsicElements` 或 `[Element]HTMLAttributes` 或 `HTMLProps` 或 `HTMLAttributes`?

ComponentProps

您可以使用 `ComponentProps` 代替 `ComponentPropsWithRef`,但您可能更喜欢明确说明组件的 refs 是否被转发,这就是我们选择演示的方式。权衡是稍微更吓人的术语。

更多信息:https://react-typescript-cheatsheet.reactjs.ac.cn/docs/basic/getting-started/forward_and_create_ref/

也许是 `React.JSX.IntrinsicElements` 或 `[Element]HTMLAttributes`

至少还有其他两种等效的方法可以做到这一点,但它们更冗长。

// Method 1: React.JSX.IntrinsicElements
type BtnType = React.JSX.IntrinsicElements["button"]; // cannot inline or will error
export interface ButtonProps extends BtnType {} // etc

// Method 2: React.[Element]HTMLAttributes
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>

查看 `ComponentProps` 的源代码 显示,这是一个巧妙的 `React.JSX.IntrinsicElements` 包装器,而第二种方法依赖于具有不熟悉命名/大小写的专用接口。

注意:有超过 50 个此类专用接口可用 - 在我们的 `@types/react` 评论 中查找 `HTMLAttributes`。

最终,我们选择了 `ComponentProps` 方法,因为它涉及最少的 TS 特定术语并且最易于使用。但是,如果您更喜欢其他方法,使用这两种方法都没问题。

绝对不是 `React.HTMLProps` 或 `React.HTMLAttributes`

这是使用 `React.HTMLProps` 时发生的情况。

export interface ButtonProps extends React.HTMLProps<HTMLButtonElement> {
specialProp: string;
}
export function Button(props: ButtonProps) {
const { specialProp, ...rest } = props;
// ERROR: Type 'string' is not assignable to type '"button" | "submit" | "reset" | undefined'.
return <button {...rest} />;
}

它为 `type` 推断出过于宽泛的 `string` 类型,因为它 在幕后使用 `AllHTMLAttributes`

这是使用 `React.HTMLAttributes` 时发生的情况。

import { HTMLAttributes } from "react";

export interface ButtonProps extends HTMLAttributes<HTMLButtonElement> {
/* etc */
}

function App() {
// Property 'type' does not exist on type 'IntrinsicAttributes & ButtonProps'
return <Button type="submit"> text </Button>;
}

包装/镜像组件

待办事项:此部分需要修改以使其更简洁。

用例:与上面相同,但对于您无法访问基础 props 的 React 组件。

import { CSSProperties } from "react";

const Box = (props: CSSProperties) => <div style={props} />;

const Card = (
{ title, children, ...props }: { title: string } & $ElementProps<typeof Box> // new utility, see below
) => (
<Box {...props}>
{title}: {children}
</Box>
);

策略:通过推断组件的 props 来提取它们。

示例

// ReactUtilityTypes.d.ts
declare type $ElementProps<T> = T extends React.ComponentType<infer Props>
? Props extends object
? Props
: never
: never;

用法

import * as Recompose from "recompose";
export const defaultProps = <
C extends React.ComponentType,
D extends Partial<$ElementProps<C>>
>(
defaults: D,
Component: C
): React.ComponentType<$ElementProps<C> & Partial<D>> =>
Recompose.defaultProps(defaults)(Component);

感谢 dmisdm

🆕 您还应该考虑是否显式转发 refs

import { forwardRef, ReactNode } from "react";

// base button, with ref forwarding
type Props = { children: ReactNode; type: "submit" | "button" };
export type Ref = HTMLButtonElement;

export const FancyButton = forwardRef<Ref, Props>((props, ref) => (
<button ref={ref} className="MyCustomButtonClass" type={props.type}>
{props.children}
</button>
));

多态组件(例如,带有 `as` props)

“多态组件” = 传递要渲染的组件,例如,使用 `as` props。

ElementType 非常有用,可以涵盖大多数可以传递给 createElement 的类型,例如:

function PassThrough(props: { as: React.ElementType<any> }) {
const { as: Component } = props;

return <Component />;
}

您也可能在 React Router 中看到这一点。

const PrivateRoute = ({ component: Component, ...rest }: PrivateRouteProps) => {
const { isLoggedIn } = useAuth();

return isLoggedIn ? <Component {...rest} /> : <Redirect to="/" />;
};

有关更多信息,您可以参考以下资源:

感谢 @eps1lon@karol-majewski 的想法!

泛型组件

就像您可以在 TypeScript 中创建泛型函数和类一样,您也可以创建泛型组件来利用类型系统实现可重用类型安全性。Props 和 State 都可以利用相同的泛型类型,尽管对于 Props 来说可能比 State 更合理。然后,您可以使用泛型类型来注释在函数/类作用域内定义的任何变量的类型。

import { ReactNode, useState } from "react";

interface Props<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}

function List<T>(props: Props<T>) {
const { items, renderItem } = props;
const [state, setState] = useState<T[]>([]); // You can use type T in List function scope.
return (
<div>
{items.map(renderItem)}
<button onClick={() => setState(items)}>Clone</button>
{JSON.stringify(state, null, 2)}
</div>
);
}

然后,您可以使用泛型组件并通过类型推断获得良好的类型安全性。

ReactDOM.render(
<List
items={["a", "b"]} // type of 'string' inferred
renderItem={(item) => (
<li key={item}>
{/* Error: Property 'toPrecision' does not exist on type 'string'. */}
{item.toPrecision(3)}
</li>
)}
/>,
document.body
);

TS 2.9 开始,您还可以在 JSX 中提供类型参数以选择退出类型推断。

ReactDOM.render(
<List<number>
items={["a", "b"]} // Error: Type 'string' is not assignable to type 'number'.
renderItem={(item) => <li key={item}>{item.toPrecision(3)}</li>}
/>,
document.body
);

您也可以使用胖箭头函数样式使用泛型。

import { ReactNode, useState } from "react";

interface Props<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}

// Note the <T extends unknown> before the function definition.
// You can't use just `<T>` as it will confuse the TSX parser whether it's a JSX tag or a Generic Declaration.
// You can also use <T,> https://github.com/microsoft/TypeScript/issues/15713#issuecomment-499474386
const List = <T extends unknown>(props: Props<T>) => {
const { items, renderItem } = props;
const [state, setState] = useState<T[]>([]); // You can use type T in List function scope.
return (
<div>
{items.map(renderItem)}
<button onClick={() => setState(items)}>Clone</button>
{JSON.stringify(state, null, 2)}
</div>
);
};

对于使用类也是一样:(鸣谢:Karol Majewskigist

import { PureComponent, ReactNode } from "react";

interface Props<T> {
items: T[];
renderItem: (item: T) => ReactNode;
}

interface State<T> {
items: T[];
}

class List<T> extends PureComponent<Props<T>, State<T>> {
// You can use type T inside List class.
state: Readonly<State<T>> = {
items: [],
};
render() {
const { items, renderItem } = this.props;
// You can use type T inside List class.
const clone: T[] = items.slice(0);
return (
<div>
{items.map(renderItem)}
<button onClick={() => this.setState({ items: clone })}>Clone</button>
{JSON.stringify(this.state, null, 2)}
</div>
);
}
}

但是您不能将泛型类型参数用于静态成员。

class List<T> extends React.PureComponent<Props<T>, State<T>> {
// Static members cannot reference class type parameters.ts(2302)
static getDerivedStateFromProps(props: Props<T>, state: State<T>) {
return { items: props.items };
}
}

要解决此问题,您需要将静态函数转换为类型推断函数。

class List<T> extends React.PureComponent<Props<T>, State<T>> {
static getDerivedStateFromProps<T>(props: Props<T>, state: State<T>) {
return { items: props.items };
}
}

为 Children 设定类型

某些 API 设计需要对传递给父组件的 `children` 进行一些限制。通常希望在类型中强制执行这些限制,但您应该意识到此功能的局限性。

您可以做什么

您可以为子元素设定 **结构** 类型:只有一个子元素,或一个子元素元组。

以下是有效的

type OneChild = React.ReactNode;
type TwoChildren = [React.ReactNode, React.ReactNode];
type ArrayOfProps = SomeProp[];
type NumbersChildren = number[];
type TwoNumbersChildren = [number, number];
别忘了,如果 TS 无法满足您的需求,您还可以使用 `prop-types`。
Parent.propTypes = {
children: PropTypes.shape({
props: PropTypes.shape({
// could share `propTypes` to the child
value: PropTypes.string.isRequired,
}),
}).isRequired,
};

您不能做什么

您不能做的事情是 **指定子元素的组件**,例如,如果您想在 TypeScript 中表达“React Router `<Routes>` 只能将 `<Route>` 作为子元素,不允许其他任何内容”这一事实。

这是因为当您编写 JSX 表达式(`const foo = <MyComponent foo='foo' />`)时,结果类型会被黑盒化为泛型 React.JSX.Element 类型。(感谢 @ferdaber

基于 Props 的类型缩减

您想要什么

// Usage
function App() {
return (
<>
{/* 😎 All good */}
<Button target="_blank" href="https://www.google.com">
Test
</Button>
{/* 😭 Error, `disabled` doesnt exist on anchor element */}
<Button disabled href="x">
Test
</Button>
</>
);
}

如何实现:使用 类型守卫

// Button props
type ButtonProps = React.ButtonHTMLAttributes<HTMLButtonElement> & {
href?: undefined;
};

// Anchor props
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement> & {
href?: string;
};

// Input/output options
type Overload = {
(props: ButtonProps): React.JSX.Element;
(props: AnchorProps): React.JSX.Element;
};

// Guard to check if href exists in props
const hasHref = (props: ButtonProps | AnchorProps): props is AnchorProps =>
"href" in props;

// Component
const Button: Overload = (props: ButtonProps | AnchorProps) => {
// anchor render
if (hasHref(props)) return <a {...props} />;
// button render
return <button {...props} />;
};

在 TypeScript Playground 中查看

组件以及 JSX 通常类似于函数。当组件可以根据其 props 呈现不同的内容时,这类似于函数可以被重载以具有多个调用签名。同样,您可以重载函数组件的调用签名以列出其所有不同的“版本”。

一个非常常见的用例是根据它是否接收 `href` 属性将某些内容呈现为按钮或锚点。

type ButtonProps = React.JSX.IntrinsicElements["button"];
type AnchorProps = React.JSX.IntrinsicElements["a"];

// optionally use a custom type guard
function isPropsForAnchorElement(
props: ButtonProps | AnchorProps
): props is AnchorProps {
return "href" in props;
}

function Clickable(props: ButtonProps | AnchorProps) {
if (isPropsForAnchorElement(props)) {
return <a {...props} />;
} else {
return <button {...props} />;
}
}

它们甚至不需要完全不同的 props,只要它们至少在属性方面存在一个差异即可。

type LinkProps = Omit<React.JSX.IntrinsicElements["a"], "href"> & {
to?: string;
};

function RouterLink(props: LinkProps | AnchorProps) {
if ("href" in props) {
return <a {...props} />;
} else {
return <Link {...props} />;
}
}
方法:泛型组件

这是一个示例解决方案,请参阅进一步讨论以了解其他解决方案。感谢 @jpavon

interface LinkProps {}
type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
type RouterLinkProps = Omit<NavLinkProps, "href">;

const Link = <T extends {}>(
props: LinkProps & T extends RouterLinkProps ? RouterLinkProps : AnchorProps
) => {
if ((props as RouterLinkProps).to) {
return <NavLink {...(props as RouterLinkProps)} />;
} else {
return <a {...(props as AnchorProps)} />;
}
};

<Link<RouterLinkProps> to="/">My link</Link>; // ok
<Link<AnchorProps> href="/">My link</Link>; // ok
<Link<RouterLinkProps> to="/" href="/">
My link
</Link>; // error
方法:组合

如果您想有条件地渲染组件,有时最好使用 React 的组合模型 来拥有更简单的组件和更容易理解的类型。

type AnchorProps = React.AnchorHTMLAttributes<HTMLAnchorElement>;
type RouterLinkProps = Omit<AnchorProps, "href">;

interface ButtonProps {
as: React.ComponentClass | "a";
children?: React.ReactNode;
}

const Button: React.FunctionComponent<ButtonProps> = (props) => {
const { as: Component, children, ...rest } = props;
return (
<Component className="button" {...rest}>
{children}
</Component>
);
};

const AnchorButton: React.FunctionComponent<AnchorProps> = (props) => (
<Button as="a" {...props} />
);

const LinkButton: React.FunctionComponent<RouterLinkProps> = (props) => (
<Button as={NavLink} {...props} />
);

<LinkButton to="/login">Login</LinkButton>;
<AnchorButton href="/login">Login</AnchorButton>;
<AnchorButton href="/login" to="/test">
Login
</AnchorButton>; // Error: Property 'to' does not exist on type...

您可能还想使用带辨识符的联合类型,请查看 使用带辨识符的联合类型创建富有表现力的 React 组件 API

以下是 **带辨识符的联合类型** 的简要直觉。

type UserTextEvent = {
type: "TextEvent";
value: string;
target: HTMLInputElement;
};
type UserMouseEvent = {
type: "MouseEvent";
value: [number, number];
target: HTMLElement;
};
type UserEvent = UserTextEvent | UserMouseEvent;
function handle(event: UserEvent) {
if (event.type === "TextEvent") {
event.value; // string
event.target; // HTMLInputElement
return;
}
event.value; // [number, number]
event.target; // HTMLElement
}
注意:TypeScript 不会基于 typeof 检查缩减带辨识符的联合类型的类型。类型守卫必须作用于键的值,而不是它的类型。
type UserTextEvent = { value: string; target: HTMLInputElement };
type UserMouseEvent = { value: [number, number]; target: HTMLElement };
type UserEvent = UserTextEvent | UserMouseEvent;
function handle(event: UserEvent) {
if (typeof event.value === "string") {
event.value; // string
event.target; // HTMLInputElement | HTMLElement (!!!!)
return;
}
event.value; // [number, number]
event.target; // HTMLInputElement | HTMLElement (!!!!)
}

上面的示例不起作用,因为我们没有检查 `event.value` 的值,而只检查了它的类型。阅读更多内容 microsoft/TypeScript#30506 (comment)

TypeScript 中的带辨识符的联合类型也可以与 React 中的钩子依赖项一起使用。当基于钩子依赖的相应联合成员发生变化时,匹配的类型会自动更新。展开更多以查看示例用例。

import { useMemo } from "react";

interface SingleElement {
isArray: true;
value: string[];
}
interface MultiElement {
isArray: false;
value: string;
}
type Props = SingleElement | MultiElement;

function Sequence(p: Props) {
return useMemo(
() => (
<div>
value(s):
{p.isArray && p.value.join(",")}
{!p.isArray && p.value}
</div>
),
[p.isArray, p.value] // TypeScript automatically matches the corresponding value type based on dependency change
);
}

function App() {
return (
<div>
<Sequence isArray={false} value={"foo"} />
<Sequence isArray={true} value={["foo", "bar", "baz"]} />
</div>
);
}
在 TS Playground 中查看

在上面的示例中,根据 `isArray` 联合成员,`value` 钩子依赖项的类型会发生变化。

为了简化这一点,您还可以将其与 **用户定义的类型守卫** 的概念结合起来。

function isString(a: unknown): a is string {
return typeof a === "string";
}

阅读手册中关于用户定义的类型守卫的更多内容。.

使用 `extends` 进行缩减

查看此快速指南:https://twitter.com/mpocock1/status/1500813765973053440?s=20&t=ImUA-NnZc4iUuPDx-XiMTA

Props:一个或另一个,但不能同时拥有

由于 TypeScript 是一个结构化类型系统,因此描述的是属性的 **最小** 要求而不是 **精确** 要求,这使得与您可能想象的相比,禁止某些 props 变得更加困难。`never` 可用于允许一个或另一个 prop,但不能同时拥有两者。

type Props1 = { foo: string; bar?: never };
type Props2 = { bar: string; foo?: never };

const OneOrTheOther = (props: Props1 | Props2) => {
if ("foo" in props && typeof props.foo === "string") {
// `props.bar` is of type `undefined`
return <>{props.foo}</>;
}
// `props.foo` is of type `undefined`
return <>{props.bar}</>;
};
const Component = () => (
<>
<OneOrTheOther /> {/* error */}
<OneOrTheOther foo="" /> {/* ok */}
<OneOrTheOther bar="" /> {/* ok */}
<OneOrTheOther foo="" bar="" /> {/* error */}
</>
);

在 TypeScript Playground 中查看.

更好的替代方案可能是使用像这样的区分符 prop。

type Props1 = { type: "foo"; foo: string };
type Props2 = { type: "bar"; bar: string };

const OneOrTheOther = (props: Props1 | Props2) => {
if (props.type === "foo") {
// `props.bar` does not exist
return <>{props.foo}</>;
}
// `props.foo` does not exist
return <>{props.bar}</>;
};
const Component = () => (
<>
<OneOrTheOther type="foo" /> {/* error */}
<OneOrTheOther type="foo" foo="" /> {/* ok */}
<OneOrTheOther type="foo" bar="" /> {/* error */}
<OneOrTheOther type="foo" foo="" bar="" /> {/* error */}
<OneOrTheOther type="bar" /> {/* error */}
<OneOrTheOther type="bar" foo="" /> {/* error */}
<OneOrTheOther type="bar" bar="" /> {/* ok */}
<OneOrTheOther type="bar" foo="" bar="" /> {/* error */}
</>
);

在 TypeScript Playground 中查看.

Props:全部传递或不传递

不传递任何 props 等同于传递一个空对象。但是,空对象的类型不是你可能认为的{}确保你理解空接口、{}Object 的含义Record<string, never> 可能最接近空对象类型,并且是typescript-eslint 推荐的。以下是一个允许“无或全部”的示例。

interface All {
a: string;
b: string;
}

type Nothing = Record<string, never>;

const AllOrNothing = (props: All | Nothing) => {
if ("a" in props) {
return <>{props.b}</>;
}
return <>Nothing</>;
};

const Component = () => (
<>
<AllOrNothing /> {/* ok */}
<AllOrNothing a="" /> {/* error */}
<AllOrNothing b="" /> {/* error */}
<AllOrNothing a="" b="" /> {/* ok */}
</>
);

虽然这可以工作,但使用Record<string, never> 表示空对象并非官方推荐。为了避免尝试类型化“一个完全空的对象”,可能最好以另一种方式处理。一种方法是将必需的 props 分组到一个可选对象中。

interface Props {
obj?: {
a: string;
b: string;
};
}

const AllOrNothing = (props: Props) => {
if (props.obj) {
return <>{props.obj.a}</>;
}
return <>Nothing</>;
};

const Component = () => (
<>
<AllOrNothing /> {/* ok */}
<AllOrNothing obj={{ a: "" }} /> {/* error */}
<AllOrNothing obj={{ b: "" }} /> {/* error */}
<AllOrNothing obj={{ a: "", b: "" }} /> {/* ok */}
</>
);

另一种方法是将两个 props 都设为可选,然后在运行时检查是否传递了全部或没有 props。

Props:仅当另一个 props 传递时才传递一个

假设你想要一个 Text 组件,如果传递了truncate props 则会被截断,但当传递了expanded props 时(例如,当用户点击文本时)会展开以显示完整文本。

你希望只有在也传递了truncate 时才允许传递expanded,因为如果文本没有被截断,expanded 就没有用处。

使用示例

const App = () => (
<>
{/* these all typecheck */}
<Text>not truncated</Text>
<Text truncate>truncated</Text>
<Text truncate expanded>
truncate-able but expanded
</Text>
{/* TS error: Property 'truncate' is missing in type '{ children: string; expanded: true; }' but required in type '{ truncate: true; expanded?: boolean | undefined; }'. */}
<Text expanded>truncate-able but expanded</Text>
</>
);

你可以通过函数重载来实现这一点。

import { ReactNode } from "react";

interface CommonProps {
children?: ReactNode;
miscProps?: any;
}

type NoTruncateProps = CommonProps & { truncate?: false };

type TruncateProps = CommonProps & { truncate: true; expanded?: boolean };

// Function overloads to accept both prop types NoTruncateProps & TruncateProps
function Text(props: NoTruncateProps): React.JSX.Element;
function Text(props: TruncateProps): React.JSX.Element;
function Text(props: CommonProps & { truncate?: boolean; expanded?: boolean }) {
const { children, truncate, expanded, ...otherProps } = props;
const classNames = truncate ? ".truncate" : "";
return (
<div className={classNames} aria-expanded={!!expanded} {...otherProps}>
{children}
</div>
);
}

Props:从类型中省略 props

注意:Omit 在 TS 3.5 中作为一等实用程序添加!🎉

有时在交叉类型时,我们想定义我们自己的 props 版本。例如,我想让我的组件有一个label,但我正在交叉的类型也具有label props。以下是提取它的方法。

export interface Props {
label: React.ReactNode; // this will conflict with the InputElement's label
}

// this comes inbuilt with TS 3.5
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;

// usage
export const Checkbox = (
props: Props & Omit<React.HTMLProps<HTMLInputElement>, "label">
) => {
const { label } = props;
return (
<div className="Checkbox">
<label className="Checkbox-label">
<input type="checkbox" {...props} />
</label>
<span>{label}</span>
</div>
);
};

当你的组件定义多个 props 时,发生冲突的可能性会增加。但是,你可以使用keyof 运算符明确声明所有字段都应从底层组件中移除。

export interface Props {
label: React.ReactNode; // conflicts with the InputElement's label
onChange: (text: string) => void; // conflicts with InputElement's onChange
}

export const Textbox = (
props: Props & Omit<React.HTMLProps<HTMLInputElement>, keyof Props>
) => {
// implement Textbox component ...
};

如你从上面的 Omit 示例中看到的,你也可以在你的类型中编写重要的逻辑。type-zoo 是一个不错的运算符工具包,你可能希望查看(包括 Omit),以及utility-types(尤其适用于从 Flow 迁移的用户)。

Props:提取组件的 Props 类型

有时你想要组件的 props 类型,但它没有导出。

一个简单的解决方案是使用React.ComponentProps

// a Modal component defined elsewhere
const defaultProps: React.ComponentProps<typeof Modal> = {
title: "Hello World",
visible: true,
onClick: jest.fn(),
};

如果你想提取组件的 props 类型并考虑内部 props、propTypesdefaultProps,则存在一些高级边缘情况 - 查看我们此处关于解决这些问题的辅助实用程序的问题

Props:渲染 Props

建议:在可能的情况下,你应该尝试使用 Hooks 而不是 Render Props。我们仅出于完整性考虑包含此内容。

有时你希望编写一个函数,该函数可以将 React 元素或字符串或其他内容作为 props。在这种情况下,最佳类型是ReactNode,它适合任何正常(嗯,React)节点适合的地方。

import { ReactNode } from "react";

interface Props {
label?: ReactNode;
children?: ReactNode;
}

const Card = ({ children, label }: Props) => {
return (
<div>
{label && <div>{label}</div>}
{children}
</div>
);
};

如果你正在使用函数作为子元素的渲染 props。

import { ReactNode } from "react";

interface Props {
children: (foo: string) => ReactNode;
}

还有什么要补充的吗?提交问题。.

处理异常

当发生不好的事情时,你可以提供良好的信息。

class InvalidDateFormatError extends RangeError {}
class DateIsInFutureError extends RangeError {}

/**
* // optional docblock
* @throws {InvalidDateFormatError} The user entered date incorrectly
* @throws {DateIsInFutureError} The user entered date in future
*
*/
function parse(date: string) {
if (!isValid(date))
throw new InvalidDateFormatError("not a valid date format");
if (isInFuture(date)) throw new DateIsInFutureError("date is in the future");
// ...
}

try {
// call parse(date) somewhere
} catch (e) {
if (e instanceof InvalidDateFormatError) {
console.error("invalid date format", e);
} else if (e instanceof DateIsInFutureError) {
console.warn("date is in future", e);
} else {
throw e;
}
}

在 TypeScript Playground 中查看

简单地抛出异常是可以的,但是最好让 TypeScript 提醒你的代码使用者处理你的异常。我们可以通过返回而不是抛出异常来做到这一点。

function parse(
date: string
): Date | InvalidDateFormatError | DateIsInFutureError {
if (!isValid(date))
return new InvalidDateFormatError("not a valid date format");
if (isInFuture(date)) return new DateIsInFutureError("date is in the future");
// ...
}

// now consumer *has* to handle the errors
let result = parse("mydate");
if (result instanceof InvalidDateFormatError) {
console.error("invalid date format", result.message);
} else if (result instanceof DateIsInFutureError) {
console.warn("date is in future", result.message);
} else {
/// use result safely
}

// alternately you can just handle all errors
if (result instanceof Error) {
console.error("error", result);
} else {
/// use result safely
}

你还可以使用特殊用途的数据类型(不要说单子……)来描述异常,例如TryOption(或Maybe)和Either 数据类型。

interface Option<T> {
flatMap<U>(f: (value: T) => None): None;
flatMap<U>(f: (value: T) => Option<U>): FormikOption<U>;
getOrElse(value: T): T;
}
class Some<T> implements Option<T> {
constructor(private value: T) {}
flatMap<U>(f: (value: T) => None): None;
flatMap<U>(f: (value: T) => Some<U>): Some<U>;
flatMap<U>(f: (value: T) => Option<U>): Option<U> {
return f(this.value);
}
getOrElse(): T {
return this.value;
}
}
class None implements Option<never> {
flatMap<U>(): None {
return this;
}
getOrElse<U>(value: U): U {
return value;
}
}

// now you can use it like:
let result = Option(6) // Some<number>
.flatMap((n) => Option(n * 3)) // Some<number>
.flatMap((n = new None())) // None
.getOrElse(7);

// or:
let result = ask() // Option<string>
.flatMap(parse) // Option<Date>
.flatMap((d) => new Some(d.toISOString())) // Option<string>
.getOrElse("error parsing string");