输入递归映射类型?

时间:2020-08-20 00:58:23

标签: typescript

(不确定标题)

我想接受任意嵌套的对象和数组,并将所有叶子位置的原语拆分为两个,目标是处理任何类型的“脏”和“原始”值,同时在两个对象之间保持一致的可导航性。原始结构和增强结构。

一个简单的例子就是

[1]     -> [{dirty: 1, pristine: 1}]
{x:[1]} -> {x: [{dirty: 1, pristine: 1}]}

类型定义似乎很容易。我具有以下通用条件类型:

class DirtyPristinePrimitive<T> {
    dirty : T; pristine: T;
    constructor(v: T) {this.dirty = v; this.pristine = v;}
}

type DirtyPristine<T> = T extends string
    ? DirtyPristinePrimitive<string>
    : T extends Number
    ? DirtyPristinePrimitive<number>
    : T extends boolean
    ? DirtyPristinePrimitive<boolean>
    : T extends null
    ? DirtyPristinePrimitive<null>
    : T extends undefined
    ? DirtyPristinePrimitive<undefined>
    : T extends Array<infer U>
    ? Array<DirtyPristine<U>>
    : {[k in keyof T]: DirtyPristine<T[k]>};

如果我在IDE中显式传递类型并对其进行自检,则上面的方法似乎可以工作,无论如何最多可以嵌套几层。

问题是编写一个函数来从某些T创建这种类型的值。我有:

function dirtyPristine<T>(v: T) : DirtyPristine<T> {
    if (Array.isArray(v)) {
        return v.map(e => dirtyPristine(e));
    }
    else if (isObject(v)) {
        const result = {};
        for (const key of Object.keys(v)) {
            (<any>result)[key] = dirtyPristine((<any>v)[key]);
        }
        return result;
    }
    else {
        return new DirtyPristinePrimitive(v);
    }
}

但是在所有3条返回语句中,编译器错误“ Type ...不可分配给DirtyPristine ”。

这可能吗?

2 个答案:

答案 0 :(得分:2)

问题在于,TypeScript编译器本质上无法评估依赖于未指定的conditional types类型参数的generic的可分配性。

当您对特定类型的值进行调用 dirtyPristine()时,编译器可以为{x: string}推断特定类型(例如T),从而求值DirtyPristine<T>(如{x: DirtyPristinePrimitive<T>})。很好。

但是在dirtyPristine()实现中,类型参数T仍然是未指定。编译器在此基本上放弃了。它推迟DirtyPristine<T>的求值,因此不知道如何确定某个值是否实际上可分配给DirtyPristine<T>。.除非已知该值是输入DirtyPristine<T>

这是TypeScript的痛点,目前尚不清楚应如何解决。有一个开放的建议microsoft/TypeScript#33912,可以在实现中使用控制流来通知编译器返回值可分配给条件返回类型。如果您希望看到这种情况,则可能需要转至问题处并给它加上?。但这似乎不一定有任何动作,即使这种情况最终发生,您也需要同时进行。

如果您确定代码会生成预期条件类型的有效实例,那么这里的解决方法是仅使用type assertions。或者,由于在每条return行上使用类型断言是令人讨厌的,因此您可以执行道德上的等效操作,并为dirtyPristine()提供一个单调用签名overload,并使实现签名足够宽松,以至于使警告安静(如(v: any) => any):

function dirtyPristine<T>(v: T): DirtyPristine<T>;
function dirtyPristine(v: any): any {
  if (Array.isArray(v)) {
    return v.map(e => dirtyPristine(e));
  }
  else if (isObject(v)) {
    const result: any = {};
    for (const key of Object.keys(v)) {
      result[key] = (dirtyPristine(v) as any)[key];
    }
    return result;
  }
  else {
    return new DirtyPristinePrimitive(v);
  }
}

请注意,这样的断言意味着您已经将编译器的责任移交给了验证类型安全性。如果您输入有误,它不会警告您,所以要小心。


无论如何,希望能有所帮助;祝你好运!

Playground link

答案 1 :(得分:0)

我没有比@jcalz的解释更好的解决方案了。

这是一个建议,它解决了一个问题((dirtyPristine(v) as any)[key]应该为dirtyPristine((v as any)[key])以匹配DirtyPristine的类型定义)并包括一些改进(IMO,但这是一个品味问题)

  • DirtyPristinePrimitive类应通过设计被限制为仅包含原始值→使用两个类型别名class DirtyPristinePrimitive<T extends primitive | nil>primitivenil,以便于阅读代码
  • DirtyPristinePrimitive类中,我更喜欢使用(必要时可以是私有的)构造函数来初始化dirtypristine属性,并使用工厂方法从单个值创建实例。
  • DirtyPristine类型定义可以使用primitivenil别名以及另一个类型别名DirtyPristineObject简化。
  • for (const key of Object.keys(v))块可以用reduce缩短
type nil = null | undefined;
type primitive = boolean | number | string;

class DirtyPristinePrimitive<T extends primitive | nil> {
  static create<T extends primitive | nil>(value: T) {
    return new DirtyPristinePrimitive(value, value);
  }

  private constructor(
    public dirty: T,
    public pristine: T,
  ) {}
}

type DirtyPristineObject<T extends object> = {
  [k in keyof T]: DirtyPristine<T[k]>
};

type DirtyPristine<T> =
  T extends primitive | nil ? DirtyPristinePrimitive<T> :
  T extends Array<any> ? Array<DirtyPristine<any>> :
  T extends object ? DirtyPristineObject<T> :
  never;

function dirtyPristine<T>(v: T): DirtyPristine<T>;
function dirtyPristine(v: any): any {
  switch (typeof v) {
    case 'boolean':
    case 'number':
      return DirtyPristinePrimitive.create(v);

    case 'object':
      if (v == null) {
        return DirtyPristinePrimitive.create(v);
      }
      if (Array.isArray(v)) {
        const res = v.map(e => dirtyPristine(e) as DirtyPristine<any>);
        return res;
      }
      return Object.keys(v).reduce(
        (result, key) => ({ ...result, [key]: dirtyPristine((v as any)[key]) }),
        {} as any);

    default:
      throw new Error('Unsupported type');
  }
}