按用例分类的有用模式
包装/镜像
包装/镜像 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} />;
}
转发 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="/" />;
};
有关更多信息,您可以参考以下资源:
- https://blog.andrewbran.ch/polymorphic-react-components/
- https://github.com/kripod/react-polymorphic-box
- https://stackoverflow.com/questions/58200824/generic-react-typescript-component-with-as-prop-able-to-render-any-valid-dom
感谢 @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 Majewski 的 gist)
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} />;
};
组件以及 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>
);
}
在上面的示例中,根据 `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 */}
</>
);
更好的替代方案可能是使用像这样的区分符 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 */}
</>
);
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、propTypes
和 defaultProps
,则存在一些高级边缘情况 - 查看我们此处关于解决这些问题的辅助实用程序的问题。
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 提醒你的代码使用者处理你的异常。我们可以通过返回而不是抛出异常来做到这一点。
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
}
你还可以使用特殊用途的数据类型(不要说单子……)来描述异常,例如Try
、Option
(或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");