typescript 如果没有采用正确的类型,则内部的泛型

dl5txlt9  于 2023-06-30  发布在  TypeScript
关注(0)|答案(1)|浏览(129)

我正在尝试实现一个函数,该函数将Map类的所有内容序列化为对象。我的意思是我试图创建一个函数,如果接收到一个对象,它将检查每个属性,如果其中一些是一个Map,它将返回一个对象。同样,如果函数接收到一个Map数组,将返回一个对象数组。
我创建了这些类型:

type InputMapToObject =
    | Map<keyof any, unknown>
    | Record<keyof any, unknown>
    | unknown[];

type ReturnMapToObject<T extends InputMapToObject> = T extends Map<
    infer KMap extends keyof any,
    infer VMap
>
    ? Record<
          KMap,
          VMap extends InputMapToObject ? ReturnMapToObject<VMap> : VMap
      >
    : T extends object
    ? {
          [P in keyof T]: T[P] extends InputMapToObject
              ? ReturnMapToObject<T[P]>
              : T[P];
      }
    : T extends (infer VArray)[]
    ? VArray extends InputMapToObject
        ? ReturnMapToObject<VArray>[]
        : VArray[]
    : never;

这种类型的作品完美,这里的测试,你可以看到,类型的工作:

interface TestingType {
    testMap: Map<string, number>;
    testArray: Array<number>;
    testArray2: Array<Map<string, string>>;
}

const test: ReturnMapToObject<TestingType> = {
    testMap: {
        string: 0,
    },
    testArray: [0],
    testArray2: [{ string: 'string' }],
};

我遇到的问题是在函数mapToObject上(其他函数只是助手):

export function mapToObject<T extends InputMapToObject>(
    itemToConvert: T
): ReturnMapToObject<T> {
    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].reduce((acc, [key, value]) => {
            acc[key] = value;
            return acc;
        }, {} as ReturnMapToObject<T>);
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) => {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        });
    }

    return getObjectEntries(itemToConvert).reduce((acc, [key, value]) => {
        if (isRecursiveValue(value)) {
            acc[key] = mapToObject(value);
        } else {
            acc[key] = value;
        }

        return acc;
    }, {} as ReturnMapToObject<T>);
}

function isRecursiveValue(item: any): item is InputMapToObject {
    if (item instanceof Map) return true;
    if (Array.isArray(item)) return true;
    if (item && typeof item === 'object') return true;
    return false;
}

function getObjectEntries<T extends Record<any, any>>(
    value: T
): [keyof any, T[keyof T]][] {
    return Object.entries(value) as [keyof any, T[keyof T]][];
}

我认为函数mapToObject在JavaScript中可以正常工作,但是我遇到了类型错误。例如,我在第一个reducer上有一个错误,如果输入是一个Map,那么我使用reduce方法将条目转换为对象。这里的问题是,我试图输入初始对象,所以我是ReturnMapToObject<T>,但TypeScript解释为T可以是任何东西,但不是,它只能是一个Map。
这是我在执行acc[key] = value时得到的错误:

Element implicitly has an 'any' type because expression of type 'string | number | symbol' can't be used to index type 'object | unknown[] | Record<string | number | symbol, unknown>'.
  No index signature with a parameter of type 'string' was found on type 'object | unknown[] | Record<string | number | symbol, unknown>'.ts(7053)
(parameter) acc: object | unknown[] | Record<string | number | symbol, unknown>

如果TypeScript正确识别Map示例中的T,如果acc为:Record<string | number | symbol, unknown>,因为它在ReturnMapToObject上定义,如果T扩展Map,那么它将返回一个Record。

z9smfwbn

z9smfwbn1#

TypeScript不能通过控制流分析narrow或重新约束generic type parameters。因此,当您检查itemToConvert instanceof Map时,itemToConvert的表观类型可以从T缩小到(类似于)T & Map,但类型参数T * 本身 * 保持不变。
这是一个很好的理由:你可以把类型看作是所有可能的可接受值的集合。让我们以string类型为例。如果我说值s是泛型类型S extends string,并且我检查s === "abc",现在我知道 values"abc",但我真的不知道关于 typeS的任何更多信息。也许S是字符串文字类型"abc",或者它可能是像"abc" | "def" | "ghi"这样的类型的联合,或者它可能是string。所有这些都与s === "abc"一致。这就像如果我从一个袋子(类型)中取出一个大理石(值),然后检查大理石的颜色,这告诉了我很多关于大理石的信息,而关于袋子和它包含的大理石的颜色的信息却很少。
因此,不能像缩小值的明显类型那样“缩小”泛型类型参数,至少在不向语言添加新功能的情况下不能这样做。在microsoft/TypeScript#27808中请求了一个这样的特性,在该特性中,您可以说T oneof InputMapToObject,这意味着T被认为是InputMapToObject的联合成员之一(假定互斥)。然后,当你检查是否是itemToConvert instanceof Map时,你可以说T本身是Map<keyof any, unknown>oneof Record<keyof any, unknown> | unknown[],其余的代码可能会按预期编译。但现在它不是语言的一部分。
当你试图实现一个依赖于泛型类型参数返回conditional type的泛型函数时,这个限制是非常明显和痛苦的,就像你正在做的ReturnMapToObject<T>一样。在microsoft/TypeScript#33912上还有一个开放的通用条件函数实现的特性请求,同样,它还不是语言的一部分。
除非这里有什么变化,否则你必须解决它。假设你不想完全重构,最方便的解决方法是使用类型Assert之类的东西来告诉编译器不要抱怨。这是合理的,如果你确定实现是正确的,但编译器不是。这也意味着你需要小心,因为如果你这样做,编译器不能捕捉到你的错误。
下面是一种进行Assert的方法:

export function mapToObject<T extends InputMapToObject>(
    itemToConvert: T
): ReturnMapToObject<T> {
    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].reduce((acc, [key, value]) => {
            acc[key] = value;
            return acc;
        }, {} as any); // <-- just use any
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) => {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        }) as ReturnMapToObject<T>; // <-- just assert
    }

    return getObjectEntries(itemToConvert).reduce((acc, [key, value]) => {
        if (isRecursiveValue(value)) {
            acc[key] = mapToObject(value);
        } else {
            acc[key] = value;
        }

        return acc;
    }, {} as any); // just use any
}

您可以尝试any类型以外的更精确的类型,这可能会捕获一些严重的错误。和/或您可以使您的函数成为单调用签名重载,这也允许比调用签名更宽松的实现:

function mapToObject<T extends InputMapToObject>(
    itemToConvert: T
): ReturnMapToObject<T>;

function mapToObject<T extends InputMapToObject>(itemToConvert: T):
    ReturnMapToObject<InputMapToObject> {

    if (itemToConvert instanceof Map) {
        return [...itemToConvert.entries()].
            reduce<Record<keyof any, unknown>>((acc, [key, value]) => {
                acc[key] = value;
                return acc;
            }, {});
    }

    if (Array.isArray(itemToConvert)) {
        return itemToConvert.map((item) => {
            if (isRecursiveValue(item)) {
                return mapToObject(item);
            }
            return item;
        })
    }

    return getObjectEntries(itemToConvert).
        reduce<Record<keyof any, unknown>>((acc, [key, value]) => {
            if (isRecursiveValue(value)) {
                acc[key] = mapToObject(value);
            } else {
                acc[key] = value;
            }
            return acc;
        }, {});
}

有许多这样的变通方法,但是如果你想保留你当前的实现,所有这些都需要你放松一些类型检查,以避免这些(可能)错误的警告。
Playground链接到代码

相关问题