在函数中使用通用类型 returns 类型 'any[]' 不可分配给类型 'T'

Use generic type in function returns Type 'any[]' is not assignable to type 'T'

我想将文档的键转换为驼峰式。

这对于 unknown 类型来说工作正常,但在一天结束时,我需要提供的类型来稍后执行操作。

camelizeKeys(strings).map(() =>...); // <- Object is of type 'unknown'. ts(2571)

我决定用通用类型更新函数,得到以下错误(注意这适用于 unknown 而不是 T): Playground Link

const isObject = (value: unknown): value is Record<string, unknown> => {
    if (value == null) {
        return false;
    }
    return typeof value === 'object';
}

const camelCase = (s: string): string => s;

const camelizeKeys = <T extends unknown>(obj: T): T => {
    if (Array.isArray(obj)) {
        return obj.map((v) => camelizeKeys(v)); // <- Type 'any[]' is not assignable to type 'T'.
    } else if (isObject(obj)) {
        return Object.keys(obj).reduce( // <- Type '{}' is not assignable to type 'T'.
            (result, key) => ({
                ...result,
                [camelCase(key)]: camelizeKeys(obj[key])
            }),
            {}
        );
    }
    return obj;
};

提前致谢,祝编码愉快!

我在此处输入时看到的问题是,您似乎有一个重载函数。如果参数是数组,则函数 return 是一个数组。如果参数是对象,则函数 return 是一个对象。这是我剥猫皮的方法。

const isObject = (value: unknown): value is Record<string, unknown> => {
  if (value == null) {
    return false;
  }
  return typeof value === "object";
};

const isArray = (value: unknown): value is unknown[] => {
  return Array.isArray(value);
};

const camelCase = (s: string): string => s;

function camelizeKeys<T>(obj: T): T;
function camelizeKeys<T>(obj: T[]): T[];
function camelizeKeys<T>(obj: any): any {
  if (isArray(obj)) {
    return obj.map((v) => camelizeKeys(v));
  } else if (isObject(obj)) {
    return Object.keys(obj).reduce(
      (result, key) => ({
        ...result,
        [camelCase(key)]: camelizeKeys(obj[key]),
      }),
      {} as T
    );
  }
  return obj;
}

camelizeKeys<T>(o: T) 是 return 类型 T,这让我有点不解。实际上情况可能并非如此。事实上,该函数应该 return 类似于 CamelizedType<T> 的类型。类似于模板文字类型在 TypeScript 4.1 中所做的事情。我找不到讨论,但是 Exploring Template Literal Types in TypeScript 4.1 看起来像是我想传达的东西。

TS Playground

在一般情况下,编译器将无法验证 camelizeKeys() 的实现是否会将类型 T 的输入值转换为类型 CamelizeKeys<T> 的输出值其中 CamelizeKeys 描述了此类转换的类型。在你的例子中,CamelizeKeys<T> 只是一个无操作 T,我猜,但下面我将展示一个实际上将 snake_case 变成 CamelCase 的版本(upper 驼峰式大小写,因为这样更容易)。无论如何:

camelizeKeys() 的实现中,泛型类型参数 Tunspecifiedunresolved。它可以是任何 T ,因为所有编译器都知道。即使您检查了 obj 的类型,编译器也无法使用该信息来缩小类型参数 T 的范围(参见 microsoft/TypeScript#13995 and microsoft/TypeScript#24085 for discussion on this). There are very few cases in which the compiler can verify that a particular value is assignable to anything related to an unspecified generic type parameter. Most of the time, and in general, the compiler will often complain that what you are doing isn't known to be safe. (See microsoft/TypeScript#33912 for a discussion of unresolved conditional generic types; I'm not sure there's a canonical issue for the general case of all unresolved generics... lots of related issues like microsoft/TypeScript#42493,但我还没有找到任何我称之为确定的东西。)


因此,对其输入类型进行任意操作的通用函数通常需要大量使用 type assertions 等来实现,以解决此问题。这意味着函数的实现者有责任保证类型安全,因为编译器不能。


举个例子,让我这样重写camelCase()

type CamelCase<T extends string> = T extends `${infer F}_${infer R}`
    ? `${Capitalize<F>}${CamelCase<R>}` : Capitalize<T>;

const camelCase = <T extends string>(s: T): CamelCase<T> =>
    s.split("_").map(x => x.charAt(0).toUpperCase() + x.slice(1)).join("") as any;

在这里,我使用 template literal types"foo_bar" 等字符串蛇形大小写文字类型转换为 FooBar 等类似的驼峰大小写文字。所以 camelCase() 作用于 T extends string 类型的值,returns 作用于 CamelCase<T> 类型的值。请注意,我必须在此处使用类型断言 as any,因为编译器无法遵循实现的逻辑来确定它是否与类型匹配。

那么,CamelizeKeys<T> 将是一个递归映射类型 key remapping:

type CamelizeKeys<T> = T extends readonly any[] ? 
{ [K in keyof T]: CamelizeKeys<T[K]> } : {
  [K in keyof T as K extends string ? CamelCase<K> : K]: CamelizeKeys<T[K]>
}

camelizeKeys() 的实现是相同的,但我在每个 return:

中给它一个强类型调用签名和一个类型断言
const camelizeKeys = <T,>(obj: T): CamelizeKeys<T> => {
    if (Array.isArray(obj)) {
        return obj.map((v) => camelizeKeys(v)) as any as CamelizeKeys<T>;
    } else if (isObject(obj)) {
        return Object.keys(obj).reduce(
            (result, key) => ({
                ...result,
                [camelCase(key)]: camelizeKeys(obj[key])
            }),
            {} as CamelizeKeys<T>
        );
    }
    return obj as any as CamelizeKeys<T>;
};

所以编译没有错误。同样,需要注意的是,如果我们在实现中犯了错误,编译器将不会注意到。无论如何,让我们测试一下:

const foo = camelizeKeys({
    prop_one: "hello",
    prop_two: { prop_three: 123 },
    prop_four: [1, "two", { prop_five: "three" }] as const
})

console.log(foo.PropOne.toUpperCase()); // HELLO
console.log(foo.PropTwo.PropThree.toFixed(2)); // 123.00
console.log(foo.PropFour[0].toFixed(2)); // 1.00
console.log(foo.PropFour[1].toUpperCase()); // TWO
console.log(foo.PropFour[2].PropFive.toUpperCase()); // THREE

这一切都按预期工作。编译器发现 foo 具有名为 PropOnePropTwoPropFour 的属性,并且所有子属性在运行时和编译时都进行了类似的转换。当然,可能会有一些极端情况,但这是我对此类事情采取的一般方法。

Playground link to code