Minor Edit: This was happening for me on TS 3.0.1
I've been running into issues using Typescript to nail down a config shape for use with a React component enhancer. In essence, I want to specify a map-object whose properties are used by the enhancer to create injected props for the enhanced component.
Where I've been running into issues seems to be trying to create mapped types derived from other mapped types. In total, for the base enhancer, I have these derivations:
And for a precomposed enhancer created for a common use case:
mapDispatchToProps
function which had as its return type props derived from a slightly different dispatch-aware version of config.The issues I tend to run into here are:
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
, probably due to lots of conditional types and mapped types.My questions then are:
Here is a small example that seems to elicit the primary errors I'm running into:
EDIT: Link to the TS playground with this in it (it just crashes the compiler on my computer/browser, though)
// 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;
}
Thanks for the help, Matt!
NOTE: fixed the example names up.
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.
That makes sense, good point, I wasn't really thinking of that at the time, kinda had my head buried deep in one way of thinking. Guess that means I can't use type params to shorten that unless I want to add more extends ...
constraints.
So, like this:
// 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]);
}
}
Unfortunately, this is crashing the tsserver request again:
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)
Alas.
I tried to simplify the example to the bare minimum to illustrate the errors, but this of course lost the original context, even if I stated the context in the description of the problem.
The original code essentially works something like this:
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);
I wanted to then use the mapped type constraints to ensure that all props on the config
shared the same OwnProps
type and that each prop itself was internally consistent with regards to the types used therein, mostly noticeable in bar
, where for instance reduce
should return the same type as its prevPropValue
argument, and that initial
should also return that same type; but also that the last array argument to reduce
is a tuple of the args types of the function returned by request
.
As part of this, I needed to then generate a type for the props that get injected by this config:
props.getAsyncData.foo(): Promise<AsyncData<APIResponse>>
props.getAsyncData.bar(barId: string): Promise<AsyncData<APIResponse>>
props.asyncData.foo: AsyncData<APIResponse>
props.asyncData.bar: AsyncData<APIResponse>
I then wanted a variation on the above config for use with a precomposition of withAsyncData
with React-Redux's connect
, which ended up looking like this:
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);
The precomposition is (essentially) just config => compose(connect(null, createMapDispatchToProps(config)), withAsyncData(createAsyncDataConfig(config)))
. But of course I need to create a base config type derived from that (slightly) extended config type using createAsyncDataConfig()
.
I don't understand the end goal; an example of input and output would be really helpful. 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. For example, a caller could do:
createBazOfRelatedMap<{x: number}, {}>(...);
And the code would try to index baz
with the property x
, which it doesn't have.
This is working for me as a solution to the original problem and hasn't crashed the compiler so far:
// 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);