【问题标题】:How to create mapped types derived from other mapped types with assignable keys如何使用可分配键创建从其他映射类型派生的映射类型
【发布时间】:2019-01-21 13:36:54
【问题描述】:

次要编辑:这发生在 TS 3.0.1 上

我在使用 Typescript 确定配置形状以与 React 组件增强器一起使用时遇到了问题。本质上,我想指定一个地图对象,其属性被增强器用来为增强组件创建注入的道具。

我遇到的问题似乎是尝试创建从其他映射类型派生的映射类型。总的来说,对于基础增强器,我有以下推导:

  • 给定一个基本配置,其道具可以是两种形状之一...
    • 创建一个规范化配置,其 props 与基本配置的 props 相同,但只有一种形状。
    • 创建一个组件道具类型,它具有...
    • props 是函数,每个基本配置 props 一个。
    • props 是值,每个基本配置 props 一个。

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

  • 一个mapDispatchToProps 函数,它的返回类型 props 派生自一个稍微不同的分派感知版本的配置。
  • 从该调度感知配置派生的基本配置。

我在这里经常遇到的问题是:

  • 键的不可分配性: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(不过,它只会使我的计算机/浏览器上的编译器崩溃)

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

至于'Extract&lt;keyof TRel, string&gt;' 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)

唉。

原始上下文

我试图将示例简化到最低限度以说明错误,但这当然丢失了原始上下文,即使我在问题描述中说明了上下文。

原始代码基本上是这样工作的:

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 类型,并且每个道具本身在内部与其中使用的类型保持一致,在@987654342 中最明显@,例如reduce 应该返回与其prevPropValue 参数相同的类型,而initial 也应该返回相同的类型;而且reduce的最后一个数组参数是request返回的函数的args类型的元组。

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

  • props.getAsyncData.foo(): Promise&lt;AsyncData&lt;APIResponse&gt;&gt;
  • props.getAsyncData.bar(barId: string): Promise&lt;AsyncData&lt;APIResponse&gt;&gt;
  • props.asyncData.foo: AsyncData&lt;APIResponse&gt;
  • props.asyncData.bar: AsyncData&lt;APIResponse&gt;

然后,我想要对上述配置进行变体,以使用 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 =&gt; compose(connect(null, createMapDispatchToProps(config)), withAsyncData(createAsyncDataConfig(config)))。但我当然需要使用createAsyncDataConfig() 创建从该(略微)扩展的配置类型派生的基本配置类型。

【问题讨论】:

  • 第一次脸红,看起来你可能试图使用BaseMapOfRelatedMap 错误。我怀疑您的调用堆栈正在爆炸,因为您试图声明一个扩展使用自身的泛型的类。
  • type BaseMapOfRelatedMap&lt;TRel extends RelatedMap&lt;TRel&gt;&gt; = ... 更改为type BaseMapOfRelatedMap&lt;TRel&gt; = ...TBase extends BaseMap&lt;TBase&gt; = ... 仅更改为TBase = ... 似乎不再增加性能,尽管第二个确实消除了第一个Type Extract&lt;...&gt; is not assignable to... 错误。但不能修复 'Extract&lt;keyof TRel, string&gt;' cannot be used to index type 'TBase' 错误。
  • 我可以在当前的master 版本的 TypeScript 中重现崩溃。我没有看到现有的匹配错误,尽管可以想象github.com/Microsoft/TypeScript/issues/22950 可能是相关的。想提出问题还是我应该提出?其他问题我会单独回答。
  • 我提交了github.com/Microsoft/TypeScript/issues/26448。哈,一个实际上与堆栈溢出有关的堆栈溢出问题!
  • 感谢您提出问题!我确实看到了另一个问题,但由于堆栈跟踪中的名称不同以及 TS 的不同版本,我不确定它是否与我的案例完全相关。 (我在 3.0.1 上,我似乎忘记在我的问题中提到的东西......)

标签: typescript mapped-types


【解决方案1】:

我不明白最终目标;输入和输出的示例将非常有帮助。至于'Extract&lt;keyof TRel, string&gt;' cannot be used to index type 'TBase'的具体错误,这是因为TRelTBase是独立的类型参数; TBase 有一个默认值,但它可以被调用者覆盖。所以没有什么可以阻止TRel 拥有TBase 没有的属性。例如,调用者可以这样做:

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

并且代码会尝试使用它没有的属性x 来索引baz

第二轮

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

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

【讨论】:

  • 感谢您指出这一点。用回复更新了我的问题。这回答了可索引性/键可分配性部分。不过,目前还不能完全确定这是否能解决所有问题。
  • 我还没有时间测试这个,迫在眉睫的最后期限等等,但我决定接受它,因为如果它不是一个完整的解决方案,它至少是一个更好的起点.谢谢!
猜你喜欢
  • 2021-08-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-02-19
相关资源
最近更新 更多