如何为 React HoC 编写类型定义
How to write type definitions for React HoCs
我有一个高阶组件可以为我处理 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 { ... }
config
和 WrappedComponent
正在获取它们的类型定义(分别为 WithFirestoreConfig
+ ComponentType<WithFirestore & Props>
。
然而,WithFirestoreConnect
并没有推断它应该是 ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>
。
我不介意定义状态两次,但这对我从 type WithFirestoreHoC<Props = {}>
到 class WithFirestoreConnect extends Component<Props, { error: Error; queries: {}; loaded: boolean }> { ... }
得到 Props
没有帮助,因为它找不到 [=19] =].
如何创建动态选择列表
WithFirestoreConfig
的一部分定义配置对象具有传递给 WrappedComponent
的道具列表
WrappedComponent: ComponentType<WithFirestore & Props>,
真的应该
WrappedComponent: ComponentType<WithFirestore & Pick<Props, config.propsPickList>,
有没有办法告诉 typescript 你在 config.propsPickList
中提供的内容将决定 WrappedComponent
应该期待什么道具?
推断 Firestore 类型
有 2 种类型的 Firestore 查询响应,一种是针对文档的,另一种是针对 Collections/Queries 的。如果这些可以在 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
的类型定义...正确的方法是什么?
React.Component 不推断类型定义: 使 Props
成为函数的类型参数而不是类型别名,然后在定义 withFirestore
.
如何创建动态选择列表: 为选择列表元素的并集添加一个 PL
类型参数。当您让 TypeScript 在调用站点推断 PL
时,这将做正确的事情,尽管调用者可能会通过将 PL
指定为包含不在实际中的元素的联合类型来产生不合理的行为列表。
推断 Firestore 类型: 我不确定您要使用 withFirestoreDocument
。您可以使用另一个 Q
类型参数和一些映射类型和条件类型来执行此操作,以从 Q
.
生成注入道具的类型
这是我对 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}</>; });
我有一个高阶组件可以为我处理 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 { ... }
config
和 WrappedComponent
正在获取它们的类型定义(分别为 WithFirestoreConfig
+ ComponentType<WithFirestore & Props>
。
然而,WithFirestoreConnect
并没有推断它应该是 ComponentClass<Props, { error: Error; queries: {}; loaded: boolean }>
。
我不介意定义状态两次,但这对我从 type WithFirestoreHoC<Props = {}>
到 class WithFirestoreConnect extends Component<Props, { error: Error; queries: {}; loaded: boolean }> { ... }
得到 Props
没有帮助,因为它找不到 [=19] =].
如何创建动态选择列表
WithFirestoreConfig
的一部分定义配置对象具有传递给 WrappedComponent
WrappedComponent: ComponentType<WithFirestore & Props>,
真的应该
WrappedComponent: ComponentType<WithFirestore & Pick<Props, config.propsPickList>,
有没有办法告诉 typescript 你在 config.propsPickList
中提供的内容将决定 WrappedComponent
应该期待什么道具?
推断 Firestore 类型
有 2 种类型的 Firestore 查询响应,一种是针对文档的,另一种是针对 Collections/Queries 的。如果这些可以在 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
的类型定义...正确的方法是什么?
React.Component 不推断类型定义: 使 Props
成为函数的类型参数而不是类型别名,然后在定义 withFirestore
.
如何创建动态选择列表: 为选择列表元素的并集添加一个 PL
类型参数。当您让 TypeScript 在调用站点推断 PL
时,这将做正确的事情,尽管调用者可能会通过将 PL
指定为包含不在实际中的元素的联合类型来产生不合理的行为列表。
推断 Firestore 类型: 我不确定您要使用 withFirestoreDocument
。您可以使用另一个 Q
类型参数和一些映射类型和条件类型来执行此操作,以从 Q
.
这是我对 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}</>; });