13
原文:Notes on TypeScript: React and Generics
作者:A. Sharif
译者:博轩

介绍

这些笔记应该有助于更好的理解 TypeScript,并且在查找如何在特定情况下使用 TypeScript 会很有帮助。所有示例都是基于 TypeScript 3.2 完成。

泛型

如果您一直在阅读“TypeScript 笔记”系列,那么到目前为止您将看到泛型的广泛使用。虽然我们一直在使用泛型,但是我们并没有真正的介绍泛型,以及它有什么用。在本系列的这一部分中,我们将首先尝试更好的理解泛型的概念,然后了解如何将泛型和 React 和 TypeScript 结合在一起更好的工作。

编写软件时,一方面是我们希望某些功能可以重用,而无需为每种可能的输入类型编写特定的功能。我们以下面这个例子作为起点:

function isDefinedNumber(a: number) : boolean {
  return a !== null || a !== undefined;
}

function isDefinedString(a: string) : boolean {
  return a!== null || a !== undefined;
}

我们不会编写显式的函数,使用 stringnumber 作为输入,而是编写一个带有一下签名的函数:

function isDefined<Type>(a: Type) : boolean {
  return a!== null || a !== undefined;
}

isDefined期望输入泛型 Type。TypeScript 将尝试推断参数,并指定一个正确的类型。让我们接着看看下一个例子,推断一下返回的类型:

function of<Type>(a: Type) : Type[] {
  return [a];
}

const toNumbers = of(1); // const toNumbers: number[]
const toStrings = of("Test Of"); // const toString: string[]

of 示例中,我们可以看到我们甚至不需要定义类型,因为 TypeScript 可以推断出参数类型。这并非适用于所有情况,有时候,我们必须明确说明类型。我们也可以这样定义上面的函数:

const toNumbers = of<number>(1); // const toNumbers: number[]
const toStrings = of<string>("Test Of"); // const toString: string[]

从技术上讲,我们可以使用 any

function of(a: any) : any {
  if (a.length !== undefined) {
    return a
  }
  return a;
}

但是使用any和泛型之间会存在很大的差异。如果你仔细看看上面的例子,我们对输入的参数一无所知。of 使用 undefined 或者 null 值调用将导致错误。泛型可以推断出确切的类型,并强制在函数体内相应地处理输入。下面是使用泛型的相同示例:

function of<Type>(a: Type) : Type[] {
  if (a.length !== undefined) { // error: Property 'length' does not exist on 'Type'
    return a
  }
  return [a];
}

在处理泛型时我们必须更加明确,可以将示例重写为以下内容:

function of<Type>(a: Type | Type[]) : Type[] {
  if (Array.isArray(a)) {
    return a
  }
  return [a];
}


const a = of(1); // const a: number[]
const b = of([1]); // const b: number[]

泛型也可以应用于重用功能,如参数 a 可以对应类型 Type 或类型数组 Type。当 1 作为参数传入时,泛型Type会绑定到 number 类型 ,当传入 [1] 时会发生同样的情况,Type 会被绑定到 number

到这里我们已经看到如何在函数中使用泛型,但同时,我们也可以将泛型与类一起使用,这使得当你在 React 中编写类组件可能会非常有趣。

class GenericClass<Type> {
  of = (a: Type | Type[]): Type[] => {
    if (Array.isArray(a)) {
      return a;
    }
    return [a];
  };
}

const genericClass = new GenericClass<number>();
const a = genericClass.of(1); // const a: number[]
const b = genericClass.of("1"); // error!
const c = genericClass.of([1]); // const c: number[]

到目前为止我们看到的示例应该有助于我们理解基础知识,我们将基于这些知识,将泛型与React组件一起使用。

React 与泛型

使用 React 时,我们可能有一个函数组件,我们需要推断参数类型。这个组件需要一个 number 或者 string 类型的参数,或者一个number 或者 string 数组类型的参数。

type RowProps<Type> = {
  input: Type | Type[];
};

function Rows<Type>({input}: RowProps<Type>) {
  if (Array.isArray(input)) {
    return <div>{input.map((i, idx) => <div key={idx}>{i}</div>)}</div>
  }
  return <div>{input}</div>
}

// usage

<Rows input={[1]} />
<Rows input={1} />
<Rows input={true} /> // Also works!

这样没问题,但是它限制依然适用于任何值。我们可以传入 true , TypeScript 不会抱怨。我们需要严格限制 Type,或者让 Type 继承 string 或者 number 类型。

function Rows<Type extends number | string>({input}: RowProps<Type>) {
  if (Array.isArray(input)) {
    return <div>{input.map((i, idx) => <div key={idx}>{i}</div>)}</div>
  }
  return <div>{input}</div>
}

<Rows input={[1]} />
<Rows input={1} />
<Rows input="1" />
<Rows input={["1"]} />
<Rows input={true} /> //Error!

我们可以确保现在只能传入预期类型的参数。值得注意的是,我们也可以为 Props 定义通用类型,如上例所示:

type RowProps<Type> = {
  input: Type | Type[];
};

接下来,我们将构建一个更高级的示例,以了解为什么泛型可以帮助我们构建可重用的React组件。我们将构建一个期望两个不同输入的组件。基于这些输入,我们将计算第三个值,并根据原始输入计算出新值,为 render 函数提供 props

type RenderPropType<InputType, OtherInputType> = { c: number } & InputType &
  OtherInputType;

type RowComponentPropTypes<InputType, OtherInputType> = {
  input: InputType;
  otherInput: OtherInputType;
  render: (props: RenderPropType<InputType, OtherInputType>) => JSX.Element;
};

第一步是定义 RowComponentPropTypes,我们用TypeScript推断参数的类型,并将 RenderPropTyperender 函数的 props 进行绑定 。RenderPropType 融合了新类型 {c: number} 产生的交集,我们将一起计算,InputTypeOtherInputType。到目前为止,我们一直在大量使用泛型。

我们可能不知道输入的确切类型,因此我们的下一步是在组件限制输入的类型。

class RowComponent<
  InputType extends { a: number },
  OtherInputType extends { b: number }
> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
  // implementation...
}

通过使用 InputType extends { a: number } 我们确保我们的输入具有提供number类型的a属性。这个规则同样适用于 OtherInputType。现在,我们实现了 RowComponent 并确保我们为 render 函数提供了 abc 三个属性。

最后,这是我们完整的示例:

class RowComponent<
  InputType extends { a: number },
  OtherInputType extends { b: number }
> extends React.Component<RowComponentPropTypes<InputType, OtherInputType>> {
  convert = (input: InputType, output: OtherInputType) => {
    return { c: input.a + output.b, ...input, ...output };
  };
  render() {
    return this.props.render(
      this.convert(this.props.input, this.props.otherInput)
    );
  }
}

<RowComponent
  input={{ a: 1 }}
  otherInput={{ b: 2 }}
  render={({ a, b, c }) => (
    <div>
      {a} {b} {c}
    </div>
  )}
/>

现在,我们对泛型,以及如何将 ReactTypeScript 配合使用,应该有个大致的了解了。

本文已经联系原文作者,并授权翻译,转载请保留原文链接

joking_zhang
2.5k 声望9.5k 粉丝

"Life's simple , you make choices and you don't look back."