Reactjs 如何为React HOC编写类型定义
我有一个高阶组件,它为我处理Firestore数据。我对typescript还不太熟悉,我很难让这些类型按我所希望的那样工作 我有几个问题:Reactjs 如何为React HOC编写类型定义,reactjs,typescript,higher-order-components,Reactjs,Typescript,Higher Order Components,我有一个高阶组件,它为我处理Firestore数据。我对typescript还不太熟悉,我很难让这些类型按我所希望的那样工作 我有几个问题: React.Component不推断类型定义: type WithFirestoreHoC<Props = {}> = ( config: WithFirestoreConfig<Props>, ) => ( WrappedComponent: ComponentType<WithFirestore &
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 { ... }
所以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),
);
使用FireStoreDocument导出常量:<
数据类型=firestore.DocumentData,
道具={}
>(
查询:FirestoreQueryable,
) => (
WrappedComponent:ComponentType,
)=>WithFirestoreHoC=query=>WrappedComponent=>
withFirestore({querys:{u默认值:query}})(
地图道具<
文档快照和道具,
{u默认值:DocumentSnapshotExpanded}&Props
>({u default,…props})=>({…props,{u default}))(WrappedComponent),
);
但是我被困在这里,因为我无法从函数的类型定义中获取
mapProp
的类型定义。。。正确的方法是什么?React.Component不推断类型定义:将Props
作为函数的类型参数,而不是类型别名,然后在使用FireStore定义时声明它
如何创建动态拾取列表:为拾取列表元素的并集添加一个PL
类型参数。当您让TypeScript在调用站点推断PL
时,这将是正确的做法,尽管调用方可以通过将PL
指定为包含不在实际列表中的元素的联合类型来产生不合理的行为
推断Firestore类型:我不确定您使用FireStoreDocument的去了哪里。您可以使用另一个Q
类型参数和一些映射类型和条件类型来执行此操作,以从Q
生成注入道具的类型
以下是我对FireStore.tsx的修订版,包括所有新功能、一些无关的修复程序,以使其在我的环境中编译,并在底部添加了一个示例(可能应该放在单独的文件中):
import*as React from'React';
从“react”导入{Component,ComponentClass,ComponentType};
进口{
文件参考,
查询
收藏参考,
文档快照已展开,
Querysnapshot
}来自“/firemodel”;
从“firebase”导入{firestore};
从“lodash”导入{pick、forEach、isEqual、isFunction};
从'modules/providers/util'导入{expandDocSnapshot,expandQuerySnapshot};
从“模块/原子/智能加载器”导入智能加载器;
类型FirestoreQueryable=
|文件参考
|质疑
|收集参考;
类型FirestoryQueryableFunction<
数据类型,
道具
> = (
firestore:firestore,firestore,
道具:道具,
)=>承诺;
类型查询配置=
FirestoreQueryable | FirestoryQueryableFunction;
类型QueryConfig={
[queryName:string]:QueryConfigEntry;
};
类型FirestoreQueryableExpanded=
QE扩展FirestoreQueryable?FirestoreQueryableExpanded1:
QE扩展了FirestoryQueryableFunction?FirestoreQueryableExpanded1:未知;
类型FirestoreQueryableExpanded1=
QE扩展了CollectionReference |查询?QuerySnapshot已展开:
QE扩展了文档引用?DocumentSnapshotExpanded:未知;
与FireStoreConfig的接口{
/**对象,该对象包含要提供给WrappedComponent的查询。
*使用的queryName也是传递快照的道具名称*/
查询:Q;
/**传递给WrappedComponent的白名单道具列表。
*没有列表的配置将白名单所有道具*/
道具?:PL[];
/**加载配置项*/
装货?:{
/**显示加载图标后的毫秒数*/
延迟?:数字;
/**显示超时消息之后的毫秒数*/
超时?:数字;
};
}
键入WithFirestoreHoC=()=>(
配置:使用FireStoreConfig,
) => (
WrappedComponent:ComponentType,
)=>组件类;
使用FireStore的常量:使用FireStoreHoc=
//需要一个额外的函数调用,以便调用方可以指定Props和
//仍然可以推断PL和Q。当需要时,可以将其移除
// https://github.com/Microsoft/TypeScript/issues/10571 实现了。
() =>
//注意:如果未传递'props',则不会对PL和it进行推断
//将默认为其约束,这正是我们想要的行为
//就打字而言。
({
询问,
道具:propPickList,
加载:{delay=200,timeout=0}={},
}:WithFirestoreConfig)=>WrappedComponent=>
使用FireStoreConnect扩展组件初始化{
订阅:{
[queryName:string]:返回类型;
} = {};
状态={
错误:null为错误,
查询:{}与FireStore一样,
加载:false,
};
componentDidMount(){
这是restartSubscription();
}
取消订阅=()=>{
forEach(this.subscriptions,unsubscribe=>unsubscribe());
this.subscriptions={};
};
restartSubscription=()=>{
//开放性问题:
//-确定何时全部加载(使用承诺?)
这个.cancelSubscriptions();
forEach(查询,异步(q:queryconfig,key)=>{
let ref:FirestoreQueryable;
if(isFunction(q)){
//这是一个异步/等待的事实意味着我们可以
//在FirestoreQueryableFunction中创建相关查询
ref=wait q(firestore(),this.props);
}否则{
//由于某些原因,收窄不起作用。
ref=q作为FirestoreQueryable;
}
if(参考firestore.DocumentReference的实例){
this.subscriptions[key]=ref.onSnapsh
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),
);
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}</>; });