键入递归映射类型?



(不确定这个标题)

我想接受任意嵌套的对象和数组,并将所有叶位置的基元一分为二,目标是处理任何类型的"脏"和"原始"值,同时通过原始和增强结构保持统一的可导航性。

一个简单的例子是这样的

[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 个返回语句中,编译器都错误为"类型 ...不能分配给 DirtyPristine"。

这可能吗?

问题是 TypeScript 编译器基本上无法评估依赖于未指定泛型类型参数的条件类型的可分配性。

当您特定类型的值调用dirtyPristine()时,编译器可以推断T的特定类型(如{x: string}),从而计算DirtyPristine<T>(如{x: DirtyPristinePrimitive<T>})。 没关系。

但是在dirtyPristine()实现中,类型参数T仍然没有指定。 编译器在这里基本上放弃了。 它推迟了DirtyPristine<T>的计算,因此不知道如何确定某个值是否真的可以分配给DirtyPristine<T>。除非已知该值属于DirtyPristine<T>类型。

这是TypeScript中的一个痛点,目前尚不清楚应该如何解决。 有一个开放的建议,microsoft/TypeScript#33912,使用实现内部的控制流来通知编译器返回值可分配给条件返回类型。 如果您希望看到这种情况发生,您可能需要转到该问题并为其提供 . 但看起来那里不一定有任何动静,即使这最终发生,你也需要在此期间继续。

此处的解决方法是,如果确定代码生成预期条件类型的有效实例,则仅使用类型断言。 或者,由于在每个return行上使用类型断言很烦人,您可以执行道德等效操作,并为dirtyPristine()提供单调用签名重载,并使实现签名足够松散以静音警告(如(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);
}
}

请注意,像这样的断言意味着你已经从编译器那里获得了验证类型安全的责任。 如果你犯了错误,它不会警告你,所以要小心。

<小时 />

游乐场链接

我没有看到比@jcalz解释得很好的解决方案更好的解决方案。

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

  • DirtyPristinePrimitive类应通过设计限制为仅包含基元值,→class DirtyPristinePrimitive<T extends primitive | nil>使用 2 种类型别名primitivenil,以便于阅读代码
  • 在类DirtyPristinePrimitive中,我更喜欢有一个(如有必要,私有的)构造函数初始化dirtypristine属性,以及一个工厂方法从单个值创建实例。
  • 可以使用primitivenil别名以及另一个类型别名DirtyPristineObject来简化DirtyPristine类型定义。
  • 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');
}
}

最新更新