如何使用可分配键创建从其他映射类型派生的映射类型
How to create mapped types derived from other mapped types with assignable keys
次要编辑:这是我在 TS 3.0.1 上发生的事情
我一直在 运行 解决使用 Typescript 确定与 React 组件增强器一起使用的配置形状的问题。本质上,我想指定一个地图对象,增强器使用其属性为增强组件创建注入道具。
我运行遇到的问题似乎是试图创建从其他映射类型派生的映射类型。总的来说,对于基础增强子,我有这些推导:
- 给定一个基本配置,其道具可能是两种形状之一...
- 创建一个标准化配置,其道具与基本配置的道具相同,但只有一种形状。
- 创建一个组件道具类型...
- 作为函数的道具,每个基本配置道具一个。
- 作为值的道具,每个基本配置道具一个。
对于为常见用例创建的预合成增强器:
- 一个
mapDispatchToProps
函数,它的 return 类型道具源自略有不同的调度感知版本的配置。
- 从该分派感知配置派生的基本配置。
我倾向于 运行 的问题是:
- 键的不可分配性:
Type 'Extract<keyof TBase, string>' is not assignable to type 'Extract<keyof TRel, string>'.
- 从相关类型派生时基类型的不可索引性:
Type 'Extract<keyof TRel, string>' cannot be used to index type 'TBase'.
- 与第一个基本相同,但在赋值点。
- TypeScript 语言服务器请求或编译器死于
RangeError: Maximum call stack size exceeded
,可能是由于大量条件类型和映射类型。
那么我的问题是:
- 这可以在 TypeScript 中完成吗?
- 如果是这样,有没有比我所做的更好的方法,同时坚持使用普通对象并仍然保留每个道具类型的约束?
- 为什么从两个映射类型派生的两组键联合是不可分配的,而当它们最终都仅从一个映射类型派生时?是不是因为即使它们都是从一个派生的,TS 只通过参数约束而不是特定的实例化类型来评估键联合可分配性?还是我遗漏了什么?
这是一个小例子,它似乎引出了我 运行 遇到的主要错误:
编辑:Link to the TS playground with this in it(它只是让我的 computer/browser 上的编译器崩溃)
// NOTE: Crashes the playground in chrome on my computer:
// RangeError: Maximum call stack size exceeded
// Also probably crashes a tsserver process/request/thing because
// vscode stops updating the error squigglies after a bit.
// Convenience.
type PropKeyOf<T> = Extract<keyof T, string>;
// A "related" type that I want to be related to a "base" type.
// Particularly, I want to be able to derive the "base" type.
// "related" and "base" are used here because these are config types
// for the interface of a related enhancer utility and
// a base enhancer utility respectively.
// They are otherwise unrelated.
type RelatedMap<T> = {
[K in PropKeyOf<T>]: RelatedMapPropType<T[K]>;
};
type RelatedMapPropType<T> = T extends RelatedMapProp<infer V> ? RelatedMapProp<V> : never;
type RelatedMapProp<V> = { foo: V, init(): V };
// A "base" type that I want to use for a "base" interface.
type BaseMap<T> = {
[K in PropKeyOf<T>]: BaseMapPropType<T[K]>;
};
type BaseMapPropType<T> = T extends BaseMapProp<infer V> ? BaseMapProp<V> : never;
type BaseMapProp<V> = { baz: V, init(): V };
// Make the conversion type
type BaseMapOfRelatedMap<TRel extends RelatedMap<TRel>> = {
[K in PropKeyOf<TRel>]: BasePropOfRelatedMapProp<TRel[K]>;
}
type BasePropOfRelatedMapProp<TRelProp> = TRelProp extends RelatedMapProp<infer V> ? BaseMapProp<V> : never;
function isOwnProp<O extends {}>(o: O, pn: string): pn is PropKeyOf<O> {
return !!o && (typeof o === 'object') && Object.prototype.hasOwnProperty.call(o, pn);
}
function createBaseMapOfRelatedMap<
TRel extends RelatedMap<TRel>,
// Error:
// - [ts] Type 'BaseMapOfRelatedMap<TRel>' does not satisfy the constraint 'BaseMap<TBase>'.
// - Type 'Extract<keyof TBase, string>' is not assignable to
// type 'Extract<keyof TRel, string>'.
TBase extends BaseMap<TBase> = BaseMapOfRelatedMap<TRel>
>(foo: TRel): TBase {
const baz = {} as TBase;
for (const propName in foo) if (isOwnProp(foo, propName)) {
// Errors:
// - [ts] Type 'Extract<keyof TRel, string>' cannot be used
// to index type 'TBase'.
// - [ts] Property 'foo' does not exist
// on type 'TRel[Extract<keyof TRel, string>]'.
baz[propName] = { baz: foo[propName].foo, init: foo[propName].init };
}
return baz;
}
编辑 1
谢谢你的帮助,马特!
注意:修复了示例名称。
在 TBase
As for the specific error that 'Extract<keyof TRel, string>' cannot be used to index type 'TBase'
, this is because TRel
and TBase
are independent type parameters; TBase
has a default, but it can be overridden by a caller. So there's nothing to prevent TRel
from having properties that TBase
does not.
说得有道理,说得好,当时我并没有真正想到这一点,我的头有点陷入一种思考方式。我猜这意味着我不能使用类型参数来缩短它,除非我想添加更多 extends ...
约束。
所以,像这样:
// added to try to typecheck created prop.
function createBasePropOfRelatedMapProp<
TRelProp extends RelatedMapProp<TRelProp>,
>(fooProp: TRelProp): BasePropOfRelatedMapProp<TRelProp> {
return { baz: fooProp.foo, init: fooProp.init };
}
function createBaseMapOfRelatedMap<
TRel extends RelatedMap<TRel>,
>(foo: TRel): BaseMapOfRelatedMap<TRel> {
const baz = {} as BaseMapOfRelatedMap<TRel>;
for (const propName in foo) if (isOwnProp(foo, propName)) {
baz[propName] = createBasePropOfRelatedMapProp(foo[propName]);
}
return baz;
}
function logBaseMap<TBase extends BaseMap<TBase>>(base: TBase): void {
for (const propName in base) if (isOwnProp(base, propName)) {
console.log(propName, '=>', base[propName]);
}
}
不幸的是,这再次导致 tsserver 请求崩溃:
Err 551 [15:35:42.708] Exception on executing command delayed processing of request 12:
Maximum call stack size exceeded
RangeError: Maximum call stack size exceeded
at getSimplifiedIndexedAccessType (/.../client/node_modules/typescript/lib/tsserver.js:37544:48)
at getSimplifiedType (/.../client/node_modules/typescript/lib/tsserver.js:37540:63)
at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:54)
at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:34)
at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
(... repeat ad nauseum)
唉。
原始上下文
我试图将示例简化到最低限度以说明错误,但这当然失去了原始上下文,即使我在问题描述中说明了上下文。
原始代码基本上是这样工作的:
const config = {
// sometimes I only need just the request itself.
foo: (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
// sometimes I need more control.
bar: {
request: (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
},
};
const enhanceComponent = withAsyncData(config);
然后我想使用映射类型约束来确保 config
上的所有道具共享相同的 OwnProps
类型,并且每个道具本身在内部与其中使用的类型保持一致,在 bar
中最引人注目,例如 reduce
应该 return 与其 prevPropValue
参数相同的类型,并且 initial
也应该 return同一类型;而且 reduce
的最后一个数组参数是由 request
编辑的函数 return 的参数类型的元组。
作为其中的一部分,我需要为通过此配置注入的道具生成一个类型:
props.getAsyncData.foo(): Promise<AsyncData<APIResponse>>
props.getAsyncData.bar(barId: string): Promise<AsyncData<APIResponse>>
props.asyncData.foo: AsyncData<APIResponse>
props.asyncData.bar: AsyncData<APIResponse>
然后我想在上面的配置上做一个变体,以便与 withAsyncData
的预合成和 React-Redux 的 connect
一起使用,最终看起来像这样:
const config = {
foo: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
bar: {
request: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
},
};
const enhanceComponent = withConnectedAsyncData(config);
预合成(本质上)只是config => compose(connect(null, createMapDispatchToProps(config)), withAsyncData(createAsyncDataConfig(config)))
。但是我当然需要使用 createAsyncDataConfig()
.
创建一个从该(稍微)扩展的配置类型派生的基本配置类型
我不明白最终目标;输入和输出的示例将非常有帮助。至于具体报错'Extract<keyof TRel, string>' cannot be used to index type 'TBase'
,这是因为TRel
和TBase
是独立的类型参数; TBase
有一个默认值,但它可以被调用者覆盖。所以没有什么可以阻止 TRel
拥有 TBase
没有的属性。例如,调用者可以这样做:
createBazOfRelatedMap<{x: number}, {}>(...);
并且代码会尝试使用 属性 x
索引 baz
,但它没有。
第 2 轮
这对我来说是解决原始问题的方法,到目前为止还没有使编译器崩溃:
// DUMMY DECLARATIONS
interface AsyncData<T> {
asyncDataMarker: T;
}
interface APIResponse {
apiResponseMarker: undefined;
}
declare function apiFetch(url: string): AsyncData<APIResponse>;
interface ComponentOwnProps {
fooId: string;
}
interface AppDispatch {
appDispatchMarker: undefined;
}
// FIRST VERSION
type SimpleConfigEntry<OwnProps, Response> = (ownProps: OwnProps) => () => Response;
type ComplexConfigEntry<OwnProps, RequestArgs extends unknown[], Response, PropValue> = {
request: (ownProps: OwnProps) => (...args: RequestArgs) => Response,
reduce: (
prevPropValue: PropValue,
nextResValue: Response,
ownProps: OwnProps,
args: RequestArgs
) => PropValue,
initial: () => PropValue
};
type CheckConfigEntry<OwnProps, T> =
T extends ComplexConfigEntry<OwnProps, infer RequestArgs, infer Response, infer PropValue>
? (ComplexConfigEntry<OwnProps, RequestArgs, Response, PropValue> extends T ? T : never)
: T extends SimpleConfigEntry<OwnProps, infer Response>
? (SimpleConfigEntry<OwnProps, Response> extends T ? T : never)
: never;
type ConfigEntryCommonInferrer<OwnProps, Response> =
((ownProps: OwnProps) => () => Response) | {request: (ownProps: OwnProps) => (...args: any[]) => Response};
declare function withAsyncData
<OwnProps, C extends {[K in keyof C]: CheckConfigEntry<OwnProps, C[K]>}>
(config: C & {[k: string]: ConfigEntryCommonInferrer<OwnProps, any>}): /*TODO*/ unknown;
type InjectedProps<C> = {
getAsyncData: {[K in keyof C]: C[K] extends ConfigEntryCommonInferrer<any, infer Response> ? Promise<Response> : unknown},
asyncData: {[K in keyof C]: C[K] extends ConfigEntryCommonInferrer<any, infer Response> ? Response : unknown}
}
// Example
const config = {
// sometimes I only need just the request itself.
foo: (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
// sometimes I need more control.
bar: {
request: (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> }),
},
};
const enhanceComponent = withAsyncData(config);
type ExampleInjectedProps = InjectedProps<typeof config>;
// SECOND VERSION
type SimpleConfigEntry2<Dispatch, OwnProps, Response> = (dispatch: Dispatch) => (ownProps: OwnProps) => () => Response;
type ComplexConfigEntry2<Dispatch, OwnProps, RequestArgs extends unknown[], Response, PropValue> = {
request: (dispatch: Dispatch) => (ownProps: OwnProps) => (...args: RequestArgs) => Response,
reduce: (
prevPropValue: PropValue,
nextResValue: Response,
ownProps: OwnProps,
args: RequestArgs
) => PropValue,
initial: () => PropValue
};
type CheckConfigEntry2<Dispatch, OwnProps, T> =
T extends ComplexConfigEntry2<Dispatch, OwnProps, infer RequestArgs, infer Response, infer PropValue>
? (ComplexConfigEntry2<Dispatch, OwnProps, RequestArgs, Response, PropValue> extends T ? T : never)
: T extends SimpleConfigEntry2<Dispatch, OwnProps, infer Response>
? (SimpleConfigEntry2<Dispatch, OwnProps, Response> extends T ? T : never)
: never;
type ConfigEntryCommonInferrer2<Dispatch, OwnProps, Response> =
((dispatch: Dispatch) => (ownProps: OwnProps) => () => Response) |
{request: (dispatch: Dispatch) => (ownProps: OwnProps) => (...args: any[]) => Response};
declare function withConnectedAsyncData
<Dispatch, OwnProps, C extends {[K in keyof C]: CheckConfigEntry2<Dispatch, OwnProps, C[K]>}>
(config: C & {[k: string]: ConfigEntryCommonInferrer2<Dispatch, OwnProps, any>}): /*TODO*/ unknown;
// Example
const config2 = {
foo: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
bar: {
request: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
},
};
const enhanceComponent2 = withConnectedAsyncData(config2);
次要编辑:这是我在 TS 3.0.1 上发生的事情
我一直在 运行 解决使用 Typescript 确定与 React 组件增强器一起使用的配置形状的问题。本质上,我想指定一个地图对象,增强器使用其属性为增强组件创建注入道具。
我运行遇到的问题似乎是试图创建从其他映射类型派生的映射类型。总的来说,对于基础增强子,我有这些推导:
- 给定一个基本配置,其道具可能是两种形状之一...
- 创建一个标准化配置,其道具与基本配置的道具相同,但只有一种形状。
- 创建一个组件道具类型...
- 作为函数的道具,每个基本配置道具一个。
- 作为值的道具,每个基本配置道具一个。
对于为常见用例创建的预合成增强器:
- 一个
mapDispatchToProps
函数,它的 return 类型道具源自略有不同的调度感知版本的配置。 - 从该分派感知配置派生的基本配置。
我倾向于 运行 的问题是:
- 键的不可分配性:
Type 'Extract<keyof TBase, string>' is not assignable to type 'Extract<keyof TRel, string>'.
- 从相关类型派生时基类型的不可索引性:
Type 'Extract<keyof TRel, string>' cannot be used to index type 'TBase'.
- 与第一个基本相同,但在赋值点。
- TypeScript 语言服务器请求或编译器死于
RangeError: Maximum call stack size exceeded
,可能是由于大量条件类型和映射类型。
那么我的问题是:
- 这可以在 TypeScript 中完成吗?
- 如果是这样,有没有比我所做的更好的方法,同时坚持使用普通对象并仍然保留每个道具类型的约束?
- 为什么从两个映射类型派生的两组键联合是不可分配的,而当它们最终都仅从一个映射类型派生时?是不是因为即使它们都是从一个派生的,TS 只通过参数约束而不是特定的实例化类型来评估键联合可分配性?还是我遗漏了什么?
这是一个小例子,它似乎引出了我 运行 遇到的主要错误:
编辑:Link to the TS playground with this in it(它只是让我的 computer/browser 上的编译器崩溃)
// NOTE: Crashes the playground in chrome on my computer:
// RangeError: Maximum call stack size exceeded
// Also probably crashes a tsserver process/request/thing because
// vscode stops updating the error squigglies after a bit.
// Convenience.
type PropKeyOf<T> = Extract<keyof T, string>;
// A "related" type that I want to be related to a "base" type.
// Particularly, I want to be able to derive the "base" type.
// "related" and "base" are used here because these are config types
// for the interface of a related enhancer utility and
// a base enhancer utility respectively.
// They are otherwise unrelated.
type RelatedMap<T> = {
[K in PropKeyOf<T>]: RelatedMapPropType<T[K]>;
};
type RelatedMapPropType<T> = T extends RelatedMapProp<infer V> ? RelatedMapProp<V> : never;
type RelatedMapProp<V> = { foo: V, init(): V };
// A "base" type that I want to use for a "base" interface.
type BaseMap<T> = {
[K in PropKeyOf<T>]: BaseMapPropType<T[K]>;
};
type BaseMapPropType<T> = T extends BaseMapProp<infer V> ? BaseMapProp<V> : never;
type BaseMapProp<V> = { baz: V, init(): V };
// Make the conversion type
type BaseMapOfRelatedMap<TRel extends RelatedMap<TRel>> = {
[K in PropKeyOf<TRel>]: BasePropOfRelatedMapProp<TRel[K]>;
}
type BasePropOfRelatedMapProp<TRelProp> = TRelProp extends RelatedMapProp<infer V> ? BaseMapProp<V> : never;
function isOwnProp<O extends {}>(o: O, pn: string): pn is PropKeyOf<O> {
return !!o && (typeof o === 'object') && Object.prototype.hasOwnProperty.call(o, pn);
}
function createBaseMapOfRelatedMap<
TRel extends RelatedMap<TRel>,
// Error:
// - [ts] Type 'BaseMapOfRelatedMap<TRel>' does not satisfy the constraint 'BaseMap<TBase>'.
// - Type 'Extract<keyof TBase, string>' is not assignable to
// type 'Extract<keyof TRel, string>'.
TBase extends BaseMap<TBase> = BaseMapOfRelatedMap<TRel>
>(foo: TRel): TBase {
const baz = {} as TBase;
for (const propName in foo) if (isOwnProp(foo, propName)) {
// Errors:
// - [ts] Type 'Extract<keyof TRel, string>' cannot be used
// to index type 'TBase'.
// - [ts] Property 'foo' does not exist
// on type 'TRel[Extract<keyof TRel, string>]'.
baz[propName] = { baz: foo[propName].foo, init: foo[propName].init };
}
return baz;
}
编辑 1
谢谢你的帮助,马特!
注意:修复了示例名称。
在 TBase
As for the specific error that
'Extract<keyof TRel, string>' cannot be used to index type 'TBase'
, this is becauseTRel
andTBase
are independent type parameters;TBase
has a default, but it can be overridden by a caller. So there's nothing to preventTRel
from having properties thatTBase
does not.
说得有道理,说得好,当时我并没有真正想到这一点,我的头有点陷入一种思考方式。我猜这意味着我不能使用类型参数来缩短它,除非我想添加更多 extends ...
约束。
所以,像这样:
// added to try to typecheck created prop.
function createBasePropOfRelatedMapProp<
TRelProp extends RelatedMapProp<TRelProp>,
>(fooProp: TRelProp): BasePropOfRelatedMapProp<TRelProp> {
return { baz: fooProp.foo, init: fooProp.init };
}
function createBaseMapOfRelatedMap<
TRel extends RelatedMap<TRel>,
>(foo: TRel): BaseMapOfRelatedMap<TRel> {
const baz = {} as BaseMapOfRelatedMap<TRel>;
for (const propName in foo) if (isOwnProp(foo, propName)) {
baz[propName] = createBasePropOfRelatedMapProp(foo[propName]);
}
return baz;
}
function logBaseMap<TBase extends BaseMap<TBase>>(base: TBase): void {
for (const propName in base) if (isOwnProp(base, propName)) {
console.log(propName, '=>', base[propName]);
}
}
不幸的是,这再次导致 tsserver 请求崩溃:
Err 551 [15:35:42.708] Exception on executing command delayed processing of request 12:
Maximum call stack size exceeded
RangeError: Maximum call stack size exceeded
at getSimplifiedIndexedAccessType (/.../client/node_modules/typescript/lib/tsserver.js:37544:48)
at getSimplifiedType (/.../client/node_modules/typescript/lib/tsserver.js:37540:63)
at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:54)
at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
at getConstraintOfDistributiveConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35523:34)
at getConstraintOfConditionalType (/.../client/node_modules/typescript/lib/tsserver.js:35535:20)
at getConstraintOfType (/.../client/node_modules/typescript/lib/tsserver.js:35496:62)
(... repeat ad nauseum)
唉。
原始上下文
我试图将示例简化到最低限度以说明错误,但这当然失去了原始上下文,即使我在问题描述中说明了上下文。
原始代码基本上是这样工作的:
const config = {
// sometimes I only need just the request itself.
foo: (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
// sometimes I need more control.
bar: {
request: (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
},
};
const enhanceComponent = withAsyncData(config);
然后我想使用映射类型约束来确保 config
上的所有道具共享相同的 OwnProps
类型,并且每个道具本身在内部与其中使用的类型保持一致,在 bar
中最引人注目,例如 reduce
应该 return 与其 prevPropValue
参数相同的类型,并且 initial
也应该 return同一类型;而且 reduce
的最后一个数组参数是由 request
编辑的函数 return 的参数类型的元组。
作为其中的一部分,我需要为通过此配置注入的道具生成一个类型:
props.getAsyncData.foo(): Promise<AsyncData<APIResponse>>
props.getAsyncData.bar(barId: string): Promise<AsyncData<APIResponse>>
props.asyncData.foo: AsyncData<APIResponse>
props.asyncData.bar: AsyncData<APIResponse>
然后我想在上面的配置上做一个变体,以便与 withAsyncData
的预合成和 React-Redux 的 connect
一起使用,最终看起来像这样:
const config = {
foo: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
bar: {
request: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
},
};
const enhanceComponent = withConnectedAsyncData(config);
预合成(本质上)只是config => compose(connect(null, createMapDispatchToProps(config)), withAsyncData(createAsyncDataConfig(config)))
。但是我当然需要使用 createAsyncDataConfig()
.
我不明白最终目标;输入和输出的示例将非常有帮助。至于具体报错'Extract<keyof TRel, string>' cannot be used to index type 'TBase'
,这是因为TRel
和TBase
是独立的类型参数; TBase
有一个默认值,但它可以被调用者覆盖。所以没有什么可以阻止 TRel
拥有 TBase
没有的属性。例如,调用者可以这样做:
createBazOfRelatedMap<{x: number}, {}>(...);
并且代码会尝试使用 属性 x
索引 baz
,但它没有。
第 2 轮
这对我来说是解决原始问题的方法,到目前为止还没有使编译器崩溃:
// DUMMY DECLARATIONS
interface AsyncData<T> {
asyncDataMarker: T;
}
interface APIResponse {
apiResponseMarker: undefined;
}
declare function apiFetch(url: string): AsyncData<APIResponse>;
interface ComponentOwnProps {
fooId: string;
}
interface AppDispatch {
appDispatchMarker: undefined;
}
// FIRST VERSION
type SimpleConfigEntry<OwnProps, Response> = (ownProps: OwnProps) => () => Response;
type ComplexConfigEntry<OwnProps, RequestArgs extends unknown[], Response, PropValue> = {
request: (ownProps: OwnProps) => (...args: RequestArgs) => Response,
reduce: (
prevPropValue: PropValue,
nextResValue: Response,
ownProps: OwnProps,
args: RequestArgs
) => PropValue,
initial: () => PropValue
};
type CheckConfigEntry<OwnProps, T> =
T extends ComplexConfigEntry<OwnProps, infer RequestArgs, infer Response, infer PropValue>
? (ComplexConfigEntry<OwnProps, RequestArgs, Response, PropValue> extends T ? T : never)
: T extends SimpleConfigEntry<OwnProps, infer Response>
? (SimpleConfigEntry<OwnProps, Response> extends T ? T : never)
: never;
type ConfigEntryCommonInferrer<OwnProps, Response> =
((ownProps: OwnProps) => () => Response) | {request: (ownProps: OwnProps) => (...args: any[]) => Response};
declare function withAsyncData
<OwnProps, C extends {[K in keyof C]: CheckConfigEntry<OwnProps, C[K]>}>
(config: C & {[k: string]: ConfigEntryCommonInferrer<OwnProps, any>}): /*TODO*/ unknown;
type InjectedProps<C> = {
getAsyncData: {[K in keyof C]: C[K] extends ConfigEntryCommonInferrer<any, infer Response> ? Promise<Response> : unknown},
asyncData: {[K in keyof C]: C[K] extends ConfigEntryCommonInferrer<any, infer Response> ? Response : unknown}
}
// Example
const config = {
// sometimes I only need just the request itself.
foo: (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
// sometimes I need more control.
bar: {
request: (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> }),
},
};
const enhanceComponent = withAsyncData(config);
type ExampleInjectedProps = InjectedProps<typeof config>;
// SECOND VERSION
type SimpleConfigEntry2<Dispatch, OwnProps, Response> = (dispatch: Dispatch) => (ownProps: OwnProps) => () => Response;
type ComplexConfigEntry2<Dispatch, OwnProps, RequestArgs extends unknown[], Response, PropValue> = {
request: (dispatch: Dispatch) => (ownProps: OwnProps) => (...args: RequestArgs) => Response,
reduce: (
prevPropValue: PropValue,
nextResValue: Response,
ownProps: OwnProps,
args: RequestArgs
) => PropValue,
initial: () => PropValue
};
type CheckConfigEntry2<Dispatch, OwnProps, T> =
T extends ComplexConfigEntry2<Dispatch, OwnProps, infer RequestArgs, infer Response, infer PropValue>
? (ComplexConfigEntry2<Dispatch, OwnProps, RequestArgs, Response, PropValue> extends T ? T : never)
: T extends SimpleConfigEntry2<Dispatch, OwnProps, infer Response>
? (SimpleConfigEntry2<Dispatch, OwnProps, Response> extends T ? T : never)
: never;
type ConfigEntryCommonInferrer2<Dispatch, OwnProps, Response> =
((dispatch: Dispatch) => (ownProps: OwnProps) => () => Response) |
{request: (dispatch: Dispatch) => (ownProps: OwnProps) => (...args: any[]) => Response};
declare function withConnectedAsyncData
<Dispatch, OwnProps, C extends {[K in keyof C]: CheckConfigEntry2<Dispatch, OwnProps, C[K]>}>
(config: C & {[k: string]: ConfigEntryCommonInferrer2<Dispatch, OwnProps, any>}): /*TODO*/ unknown;
// Example
const config2 = {
foo: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => () => apiFetch(`/api/foos/${ownProps.fooId}`),
bar: {
request: (dispatch: AppDispatch) => (ownProps: ComponentOwnProps) => (barId: string) => apiFetch(`/api/foos/${ownProps.fooId}/bars/${barId}`),
reduce: (
prevPropValue: { [k: string]: AsyncData<APIResponse> },
nextResValue: AsyncData<APIResponse>,
ownProps: ComponentOwnProps,
[barId]: [string]
) => ({
...prevPropValue,
[barId]: nextResValue,
}),
initial: () => ({} as { [k: string]: AsyncData<APIResponse> })
},
};
const enhanceComponent2 = withConnectedAsyncData(config2);