简体   繁体   English

在 function 中使用泛型类型返回类型“任何 []”不可分配给类型“T”

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

I want to convert the keys of a document to camelCase.我想将文档的键转换为 camelCase。

This was working fine with an unknown type, but at the end of the day, I need the type provided to perform operations later.这对unknown类型运行良好,但归根结底,我需要提供的类型以便稍后执行操作。

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

I decided to update the function with generic typings, getting the following errors (note that this works with unknown instead of T ): Playground Link我决定用通用类型更新 function,得到以下错误(请注意,这适用于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;
};

Thanks in advance and happy coding!在此先感谢并祝您编码愉快!

The problem I see with typing here is, it seems like you have an overloaded function.我在这里输入时看到的问题是,您似乎有一个重载的 function。 The function returns an array if parameter is an array.如果参数是一个数组,function 返回一个数组。 The function returns an object if the parameter is object.如果参数为 object,则 function 返回 object。 Here is my way to skin the cat.这是我给猫剥皮的方法。

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;
}

It nags me a bit that camelizeKeys<T>(o: T) is returning type T. In reality that may not be the case. camelizeKeys<T>(o: T)返回类型 T 让我有点恼火。实际上可能并非如此。 In fact, the function should return a type something like CamelizedType<T> .事实上, function 应该返回类似于CamelizedType<T>的类型。 Something like what Template Literal Types does in TypeScript 4.1 onwards.类似于模板文字类型在 TypeScript 4.1 及更高版本中所做的事情。 I could not find the discussion, but Exploring Template Literal Types in TypeScript 4.1 looks like something what I want to convey.我找不到讨论,但Exploring Template Literal Types in TypeScript 4.1看起来像是我想要传达的东西。

TS Playground TS游乐场

In the general case, the compiler will not be able to verify that the implementation of camelizeKeys() will transform the input value of type T into an output value of type CamelizeKeys<T> where CamelizeKeys describes the type of such transformation.在一般情况下,编译器将无法验证camelizeKeys()的实现是否会将T类型的输入值转换为CamelizeKeys<T>类型的 output 值,其中CamelizeKeys描述了这种转换的类型。 In your example, CamelizeKeys<T> would just be a no-op T , I guess, but below I will present a version that actually turns snake_case into CamelCase ( upper camel case, because that's easier).在您的示例中,我猜CamelizeKeys<T>将只是一个无操作T ,但在下面我将展示一个实际上将snake_caseCamelCase的版本(大骆驼大小写,因为这更容易)。 Regardless:不管:

Inside the implementation of camelizeKeys() , the generic type parameter T is unspecified or unresolved .camelizeKeys()的实现中,泛型类型参数Tunspecifiedunresolved It could be any T at all, for all the compiler knows.它可以是任何T ,因为所有编译器都知道。 Even if you check the type of obj , the compiler cannot use that information to narrow the type parameter T (see microsoft/TypeScript#13995 and microsoft/TypeScript#24085 for discussion on this).即使您检查了obj的类型,编译器也无法使用该信息来缩小类型参数T (有关此问题的讨论,请参见microsoft/TypeScript#13995microsoft/TypeScript#24085 )。 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 but I haven't found anything I'd call definitive.) (有关未解决的条件泛型类型的讨论,请参阅microsoft/TypeScript#33912 ;我不确定所有未解决的 generics 的一般情况是否存在规范问题......很多相关问题,如microsoft/TypeScript#42493但我没有没有找到任何我认为是确定性的东西。)


So, generic functions that do arbitrary manipulation of their input type will often need to be implemented with heavy use of type assertions , or the like, to work around this.因此,对其输入类型进行任意操作的通用函数通常需要通过大量使用类型断言等来实现,以解决此问题。 It means that the implementer of the function has the responsibility to guarantee type safety, since the compiler cannot.这意味着 function 的实现者有责任保证类型安全,因为编译器不能。


As an example, let me rewrite camelCase() this way:例如,让我这样重写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;

Here I'm using template literal types to turn string snake case literal types like "foo_bar" into the analogous camel case literals like FooBar .在这里,我使用模板文字类型将字符串蛇大小写文字类型(如"foo_bar"转换为类似的骆驼大小写文字(如FooBar So camelCase() acts on a value of type T extends string and returns a value of type CamelCase<T> .所以camelCase()作用于T extends string类型的值并返回CamelCase<T>类型的值。 Note that I had to use a type assertion as any here because again, the compiler has no way to follow the logic of the implementation to determine if it matches the typings.请注意,我必须在这里使用类型断言as any因为编译器无法遵循实现的逻辑来确定它是否与类型匹配。

Then, CamelizeKeys<T> will be a recursive mapped type with key remapping :然后, CamelizeKeys<T>将是具有键重新映射的递归映射类型:

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]>
}

And the implmentation of camelizeKeys() is the same, but I give it a strongly typed call signature and a type assertion in every return : 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>;
};

So that compiles with no error.所以编译没有错误。 Again, the caveat is that if we made a mistake in the implementation, the compiler will not notice.同样,需要注意的是,如果我们在实现中犯了错误,编译器将不会注意到。 Anyway, let's test it:无论如何,让我们测试一下:

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

This all works as expected.这一切都按预期工作。 The compiler sees that foo has properties named PropOne , PropTwo , and PropFour , and all subproperties are similarly transformed, both at runtime and at compile time.编译器看到foo具有名为PropOnePropTwoPropFour的属性,并且所有子属性在运行时和编译时都进行了类似的转换。 There may well be edge cases, of course, but this is the general approach I would take for something like this.当然,很可能存在边缘情况,但这是我对此类事情采取的一般方法。

Playground link to code Playground 代码链接

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM