如何使用可分配键创建从其他映射类型派生的映射类型

How to create mapped types derived from other mapped types with assignable keys

次要编辑:这是我在 TS 3.0.1 上发生的事情

我一直在 运行 解决使用 Typescript 确定与 React 组件增强器一起使用的配置形状的问题。本质上,我想指定一个地图对象,增强器使用其属性为增强组件创建注入道具。

我运行遇到的问题似乎是试图创建从其他映射类型派生的映射类型。总的来说,对于基础增强子,我有这些推导:

对于为常见用例创建的预合成增强器:

我倾向于 运行 的问题是:

那么我的问题是:

这是一个小例子,它似乎引出了我 运行 遇到的主要错误:

编辑: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 的参数类型的元组。

作为其中的一部分,我需要为通过此配置注入的道具生成一个类型:

然后我想在上面的配置上做一个变体,以便与 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',这是因为TRelTBase是独立的类型参数; 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);