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

时间:2018-08-14 16:16:34

标签: typescript mapped-types

次要修改:这是我在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'.派生时,基本类型的不可索引性
    • 与第一个基本相同,但在分配时。
  • TypeScript语言服务器请求或编译器死于RangeError: Maximum call stack size exceeded,可能是由于许多条件类型和映射类型所致。

那么我的问题是:

  • 这可以在TypeScript中完成吗?
  • 如果是这样,有比我既坚持朴素的对象又仍然保留按prop类型约束的方法更好的方法了?
  • 为什么从两种映射类型派生的两组键联合最终都只能从一种映射类型派生而又不可分配?是因为即使它们都是从一个派生的,TS还是仅通过参数约束而不是特定的实例化类型来评估键联盟的可分配性?还是我想念的其他东西?

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

编辑: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;
}

编辑1

Matt,谢谢您的帮助!

注意:修复示例名称。

TBase

  

对于'Extract<keyof TRel, string>' cannot be used to index type 'TBase'的特定错误,这是因为TRelTBase是独立的类型参数; 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()创建从(略)扩展的配置类型派生的基本配置类型。

1 个答案:

答案 0 :(得分:1)

我不了解最终目标;输入和输出的示例将非常有帮助。至于'Extract<keyof TRel, string>' cannot be used to index type 'TBase'的特定错误,这是因为TRelTBase是独立的类型参数。 TBase是默认设置,但可以被调用方覆盖。因此,没有什么可以阻止TRel具有TBase没有的属性。例如,呼叫者可以执行以下操作:

createBazOfRelatedMap<{x: number}, {}>(...);

然后代码将尝试使用属性bazx编制索引,而该属性却没有。

第二回合

这对我来说是一个解决原始问题的方法,到目前为止尚未使编译器崩溃:

// 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);