次要修改:这是我在TS 3.0.1上发生的
我一直在使用Typescript来确定与React组件增强器一起使用的配置形状时遇到问题。本质上,我想指定一个地图对象,其属性由增强器用来为增强组件创建注入的道具。
我遇到问题的地方似乎正在尝试创建从其他映射类型派生的映射类型。总体而言,对于基础增强剂,我有以下推导:
对于为普通用例创建的预组合增强器:
mapDispatchToProps
函数,其返回类型是从略微不同的可感知分发版本的config派生的道具。我倾向于在这里遇到的问题是:
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'.
派生时,基本类型的不可索引性
RangeError: Maximum call stack size exceeded
,可能是由于许多条件类型和映射类型所致。那么我的问题是:
这是一个小例子,似乎引发了我遇到的主要错误:
编辑:Link to the TS playground with this in it(虽然它只会使我的计算机/浏览器上的编译器崩溃)
// 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;
}
Matt,谢谢您的帮助!
注意:修复示例名称。
TBase
对于
'Extract<keyof TRel, string>' cannot be used to index type 'TBase'
的特定错误,这是因为TRel
和TBase
是独立的类型参数;TBase
是默认设置,但可以被调用方覆盖。因此,没有什么可以阻止TRel
具有TBase
没有的属性。
这是有道理的,很好的一点,当时我并没有真正想到,有点以一种思考的方式将我的头埋在了深处。猜猜这意味着除非我想添加更多的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)
A。
我试图将示例简化到最小程度以说明错误,但是即使我在问题描述中说明了上下文,这当然也会丢失原始上下文。
原始代码本质上是这样的:
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
应该返回与其prevPropValue
参数相同的类型,而initial
也应该返回相同的类型;而且reduce
的最后一个数组参数是request
返回的函数的args类型的元组。
为此,我需要为通过此配置注入的道具生成类型:
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()
创建从(略)扩展的配置类型派生的基本配置类型。
答案 0 :(得分:1)
我不了解最终目标;输入和输出的示例将非常有帮助。至于'Extract<keyof TRel, string>' cannot be used to index type 'TBase'
的特定错误,这是因为TRel
和TBase
是独立的类型参数。 TBase
是默认设置,但可以被调用方覆盖。因此,没有什么可以阻止TRel
具有TBase
没有的属性。例如,呼叫者可以执行以下操作:
createBazOfRelatedMap<{x: number}, {}>(...);
然后代码将尝试使用属性baz
为x
编制索引,而该属性却没有。
这对我来说是一个解决原始问题的方法,到目前为止尚未使编译器崩溃:
// 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);