【问题标题】:How to write type definitions for React HoCs如何为 React HoC 编写类型定义
【发布时间】:2018-09-20 20:52:08
【问题描述】:

我有一个为我处理 Firestore 数据的高阶组件。我对打字稿很陌生,我无法让这些类型按我希望的方式工作。

Here are the full files + some extra ts definitions

我有几个问题:

React.Component 不推断类型定义:

type WithFirestoreHoC<Props = {}> = (
  config: WithFirestoreConfig<Props>,
) => (
  WrappedComponent: ComponentType<WithFirestore & Props>,
) => ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>;

const withFirestore: WithFirestoreHoC = ({
  queries,
  props: propPickList,
  loading: { delay = 200, timeout = 0 } = {},
}) => WrappedComponent =>
  class WithFirestoreConnect extends Component { ... }

configWrappedComponent 正在获取它们的类型定义(分别为 WithFirestoreConfig + ComponentType&lt;WithFirestore &amp; Props&gt;

但是,WithFirestoreConnect 并没有推断它应该是ComponentClass&lt;Props, { error: Error; queries: {}; loaded: boolean }&gt;

我不介意将状态定义两次,但这无助于将Propstype WithFirestoreHoC&lt;Props = {}&gt; 转换为class WithFirestoreConnect extends Component&lt;Props, { error: Error; queries: {}; loaded: boolean }&gt; { ... },因为它找不到Props

如何创建动态选择列表

WithFirestoreConfig 的一部分定义配置对象具有传递给WrappedComponent 的道具列表

WrappedComponent: ComponentType&lt;WithFirestore &amp; Props&gt;, 真的应该 WrappedComponent: ComponentType&lt;WithFirestore &amp; Pick&lt;Props, config.propsPickList&gt;,

有没有办法告诉 typescript 你在config.propsPickList 中提供的内容将决定WrappedComponent 应该期待什么道具?

推断 Firestore 类型

Firestore 查询响应有 2 种类型,一种用于文档,一种用于集合/查询。如果可以在config.queries 中将它们定义为如下所示,那就太棒了:

{ queries: { 
    docQuery: myDocument as DocumentReference<docDataType>, 
    collectionQuery: myDocument as CollectionReference<docDataType>,  
} }

因此 WrappedComponent 可以知道在另一端是否期望查询或文档数据结构。

这似乎超级复杂,所以我在这里有一个更简单的示例(它是创建单个订阅的快捷方式),这至少是实现我想要的一个很好的垫脚石:

export const withFirestoreDocument: <
  DataType = firestore.DocumentData,
  Props = {}
>(
  query: FirestoreQueryable<DataType>,
) => (
  WrappedComponent: ComponentType<DocumentSnapshotExpanded<DataType>>,
) => WithFirestoreHoC<Props> = query => WrappedComponent =>
  withFirestore({ queries: { _default: query } })(
    mapProps<
      DocumentSnapshotExpanded<DataType> & Props,
      { _default: DocumentSnapshotExpanded<DataType> } & Props
    >(({ _default, ...props }) => ({ ...props, ..._default }))(WrappedComponent),
  );

但是我被困在这里,因为我无法从函数的类型定义中获取 mapProp 的类型定义......这样做的正确方法是什么?

【问题讨论】:

    标签: reactjs typescript higher-order-components


    【解决方案1】:

    React.Component 不推断类型定义:Props 设为函数的类型参数而不是类型别名,然后在定义 withFirestore 时声明它。

    如何创建动态选择列表:为选择列表元素的并集添加PL 类型参数。当您让 TypeScript 在调用站点推断 PL 时,这将做正确的事情,尽管调用者可能会通过将 PL 指定为联合类型(包括不在实际列表中的元素)来产生不合理的行为。

    推断 Firestore 类型:我不确定您使用 withFirestoreDocument 的目的。您可以使用另一个 Q 类型参数和一些映射类型和条件类型来执行此操作,以从 Q 生成注入的 props 的类型。

    这是我对withFirestore.tsx 的修订版,其中包含所有新功能、一些不相关的修复以使其在我的环境中编译,并在底部添加了一个示例(可能应该放在单独的文件中):

    import * as React from 'react';
    import { Component, ComponentClass, ComponentType } from 'react';
    import {
      DocumentReference,
      Query,
      CollectionReference,
      DocumentSnapshotExpanded,
      QuerySnapshotExpanded
    } from './firemodel';
    import { firestore } from 'firebase';
    import { pick, forEach, isEqual, isFunction } from 'lodash';
    import { expandDocSnapshot, expandQuerySnapshot } from 'modules/providers/util';
    import SmartLoader from 'modules/atoms/SmartLoader';
    
    type FirestoreQueryable<DataType> =
      | DocumentReference<DataType>
      | Query<DataType>
      | CollectionReference<DataType>;
    
    type FirestoryQueryableFunction<
      DataType,
      Props
    > = (
      firestore: firestore.Firestore,
      props: Props,
    ) => Promise<FirestoreQueryable<DataType>>;
    
    type QueryConfigEntry<Props> =
      FirestoreQueryable<any> | FirestoryQueryableFunction<any, Props>;
    
    type QueryConfig<Props> = {
      [queryName: string]: QueryConfigEntry<Props>;
    };
    
    type FirestoreQueryableExpanded<Props, QE extends QueryConfigEntry<Props>> =
      QE extends FirestoreQueryable<any> ? FirestoreQueryableExpanded1<QE> :
      QE extends FirestoryQueryableFunction<any, Props> ? FirestoreQueryableExpanded1<ReturnType<QE>> : unknown;
    
    type FirestoreQueryableExpanded1<QE extends FirestoreQueryable<any>> =
      QE extends CollectionReference<infer DataType> | Query<infer DataType> ? QuerySnapshotExpanded<DataType> :
      QE extends DocumentReference<infer DataType> ? DocumentSnapshotExpanded<DataType> : unknown;
    
    interface WithFirestoreConfig<Props, PL extends keyof Props, Q extends QueryConfig<Props>> {
      /** Object containing the queries to be provided to WrappedComponent.
       * The queryName used is also the prop name the snapshot is passed in. */
      queries: Q;
      /** A list of props to whitelist passing to WrappedComponent.
       * Configs without a list will whitelist all props */
      props?: PL[];
      /** Loading config items */
      loading?: {
        /** Number of ms after which to display the loading icon */
        delay?: number;
        /** Number of ms after which to display the timeout message */
        timeout?: number;
      };
    }
    
    type WithFirestoreHoC = <Props>() => <PL extends keyof Props, Q extends QueryConfig<Props>>(
      config: WithFirestoreConfig<Props, PL, Q>,
    ) => (
      WrappedComponent: ComponentType<WithFirestore<Props, Q> & Pick<Props, PL>>,
    ) => ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>;
    
    const withFirestore: WithFirestoreHoC =
      // An extra function call is needed so that callers can specify Props and
      // still have PL and Q inferred.  It can be removed when
      // https://github.com/Microsoft/TypeScript/issues/10571 is implemented.
      <Props extends {}>() =>
      // Note: if `props` is not passed, there will be no inference for PL and it
      // will default to its constraint, which is exactly the behavior we want as
      // far as typing is concerned.
      <PL extends keyof Props, Q extends QueryConfig<Props>>({
        queries,
        props: propPickList,
        loading: { delay = 200, timeout = 0 } = {},
      }: WithFirestoreConfig<Props, PL, Q>) => WrappedComponent =>
      class WithFirestoreConnect extends Component<Props, { error: Error; queries: WithFirestore<Props, Q>; loaded: boolean }> {
        subscriptions: {
          [queryName: string]: ReturnType<FirestoreQueryable<any>['onSnapshot']>;
        } = {};
        state = {
          error: null as Error,
          queries: {} as WithFirestore<Props, Q>,
          loaded: false,
        };
        componentDidMount() {
          this.restartSubscription();
        }
    
        cancelSubscriptions = () => {
          forEach(this.subscriptions, unsubscribe => unsubscribe());
          this.subscriptions = {};
        };
    
        restartSubscription = () => {
          // Open questions:
          //   - figuring out when all loaded (use a promise?)
          this.cancelSubscriptions();
          forEach(queries, async (q: QueryConfigEntry<Props>, key) => {
            let ref: FirestoreQueryable<any>;
            if (isFunction(q)) {
              // The fact that this is an async/await means that we can
              // create dependent queries within our FirestoreQueryableFunction
              ref = await q(firestore(), this.props);
            } else {
              // Narrowing is not working for some reason.
              ref = q as FirestoreQueryable<any>;
            }
            if (ref instanceof firestore.DocumentReference) {
              this.subscriptions[key] = ref.onSnapshot(
                snap => {
                  this.setState({
                    queries: Object.assign({}, this.state.queries, {[key]: expandDocSnapshot(snap)}),
                  });
                },
                err => {
                  console.error(JSON.stringify(err));
                  this.setState({ error: err });
                  this.cancelSubscriptions();
                },
              );
            } else if (
              ref instanceof firestore.CollectionReference ||
              ref instanceof firestore.Query
            ) {
              let ref2: {onSnapshot(os: (snap: firestore.QuerySnapshot) => void, oe: (err: Error) => void): () => void; } = ref;
              this.subscriptions[key] = ref2.onSnapshot(
                snap => {
                  this.setState({
                    queries: Object.assign({}, this.state.queries, {[key]: expandQuerySnapshot(snap)}),
                  });
                },
                err => {
                  console.error(JSON.stringify(err));
                  this.setState({ error: err });
                  this.cancelSubscriptions();
                },
              );
            }
          });
        };
    
        componentDidUpdate(prevProps: Props) {
          if (!isEqual(this.props, prevProps)) {
            this.restartSubscription();
          }
        }
        componentWillUnmount() {
          this.cancelSubscriptions();
        }
        render() {
          if (!this.state.loaded || this.state.error) {
            return (
              <SmartLoader
                error={this.state.error}
                timeout={timeout}
                delay={delay}
              />
            );
          }
    
          const whitelistedProps = propPickList
            ? pick(this.props, propPickList)
            : this.props;
          // Unsure what's wrong here ~ Matt
          let WrappedComponent2 = WrappedComponent as any;
          return <WrappedComponent2 {...whitelistedProps} {...this.state.queries} />;
        }
      };
    
    export type WithFirestore<Props, Q extends QueryConfig<Props>> = {
      [queryName in keyof Q]: FirestoreQueryableExpanded<Props, Q[queryName]>;
    }
    
    export default withFirestore;
    
    // EXAMPLE
    
    interface MyDoc {
      y: number
    }
    declare let myDocRef: DocumentReference<MyDoc>;
    declare let myCollRef: CollectionReference<MyDoc>;
    let wrapped = withFirestore<{x: string}>()({
      queries: {
        myDoc: myDocRef,
        myColl: myCollRef
      },
    })((props) => { return <>{props.myDoc.data.y + props.myColl.docs[props.x].data.y}</>; });
    

    【讨论】:

    • 哇,这非常有帮助。它工作得很好,打字稿支持比我的第一次迭代要好得多。但是,我不确定我是否学到了任何东西,因为这对我来说绝对太先进了。你知道关于 typescript + react 复杂使用的任何指南/教程吗?
    • 这只是高级的 TypeScript 类型推断和操作,并不特定于 React。我将从the handbook 中的示例开始,用于映射和条件类型。对于引入 QPL 之类的类型变量以保留类型信息的技术,我没有资源可以指出,但这个想法在 Stack Overflow 上已经多次出现。如果您在浏览手册页面后对解决方案有任何具体问题,我很乐意在这里回答。
    猜你喜欢
    • 2017-08-25
    • 1970-01-01
    • 2022-01-18
    • 1970-01-01
    • 2019-10-05
    • 2017-11-05
    • 2017-07-21
    • 1970-01-01
    • 2022-08-19
    相关资源
    最近更新 更多