admin管理员组

文章数量:1123427

I have the following function that transforms array elements into objects:

function mapToCreateObjects<K extends keyof U, T, U extends Record<K, T[]>>(
  obj: U,
  key: K,
): Record<K, { create: T }[]> {
  return {
    [key]: obj[key].map((item) => ({ create: item })),
  } as Record<K, { create: T }[]>;
}

const data = {
  fruits: ['apple', 'banana', 'cherry'],
  vegetables: [1, 2, 3],
};

const transformedFruits = {
  ...mapToCreateObjects(data, 'fruits'),
  ...mapToCreateObjects(data, 'vegetables'),
};

I expect transformedFruits to have this type:

{ fruits: { create: string }[], vegetables: { create: number }[] }

But TypeScript infers { create: unknown }[] for both. Why is TypeScript inferring unknown and how can I fix it to infer the correct types for create?

I have the following function that transforms array elements into objects:

function mapToCreateObjects<K extends keyof U, T, U extends Record<K, T[]>>(
  obj: U,
  key: K,
): Record<K, { create: T }[]> {
  return {
    [key]: obj[key].map((item) => ({ create: item })),
  } as Record<K, { create: T }[]>;
}

const data = {
  fruits: ['apple', 'banana', 'cherry'],
  vegetables: [1, 2, 3],
};

const transformedFruits = {
  ...mapToCreateObjects(data, 'fruits'),
  ...mapToCreateObjects(data, 'vegetables'),
};

I expect transformedFruits to have this type:

{ fruits: { create: string }[], vegetables: { create: number }[] }

But TypeScript infers { create: unknown }[] for both. Why is TypeScript inferring unknown and how can I fix it to infer the correct types for create?

Share Improve this question edited 15 hours ago jonrsharpe 122k30 gold badges264 silver badges472 bronze badges asked 16 hours ago Mathieu LordonMathieu Lordon 3071 silver badge7 bronze badges 3
  • 2 TypeScript sometimes infers unknown on very complex types. It's frustrating for sure. I don't have an answer, but I can say as a TS dev of 4+ years that your code looks good to me, and I'd just cast the results as needed. Make sure you're using a recent version of TS, it may have improvements here :) – Mark Wiemer Commented 16 hours ago
  • 3 There's nowhere in your code from where TS can possibly infer T, because constraints are not used as inference sites, see stackoverflow.com/q/76294709/2887218. You should drop T, only infer U and K and then compute T from it (which is U[K][number]), as shown in this playground link. Does that fully address the question? If so I'll write an answer explaining. If not, what's missing? – jcalz Commented 14 hours ago
  • Thank you for the explanation, the playground link, and the reference to the related question. Using U[K][number] to compute T works perfectly. I appreciate your help in clarifying this! – Mathieu Lordon Commented 14 hours ago
Add a comment  | 

2 Answers 2

Reset to default 2

I defer to @Mark Wiemer's comment about Typescript inferring unknown. I found that I could get it to return the correct types by using typeof obj[K][number] in the return type.

function mapToCreateObjects<K extends keyof U, T, U extends Record<K, T[]>>(
    obj: U,
    key: K,
): Record<K, { create: typeof obj[K][number] }[]> {
  return {
    [key]: obj[key].map((item) => ({ create: item })),
  } as Record<K, { create: typeof obj[K][number] }[]>;
}

The transformedFruits variable now has this type:

{ vegetables: { create: number }[], fruits: { create: string }[] }

I hope this is helpful!

TypeScript does not use generic constraints as inference sites for other generic type parameters. That is, U extends Record<K, T[]> does not help TypeScript infer T. Given the call signature

<K extends keyof U, T, U extends Record<K, T[]>>(
  obj: U, key: K
) => Record<K, { create: T }[]>

if you call that function like f(obj, key), TypeScript can infer U from the type of obj, and K from the type of key. But T cannot be inferred from anywhere at all. There's no argument whose type is directly related to T. Since T has no inference site, inference just fails, and it falls back to its own constraint (and since T has no explicit constraint, it uses the implicit constraint) which is unknown.

All U extends Record<K, T[]> accomplishes is to check U against Record<typeof key, unknown[]> after it's too late.


There is an old feature request at microsoft/TypeScript#7234 to treat constraints as inference sites, but it was eventually closed, as you can almost always rewrite your generics in a way the TypeScript can infer, often with intersections (e.g., rewrite <T, U extends F<T>>(u: U) => void as <T, U extends F<T>>(u: U & F<T>) => void).

In your example, it looks like T really isn't needed as its own generic type parameter. You have all the information you need from K and U. That is, since U is a Record<K, T[]>, then U[K] is assignable to T[], so U[K][number] is assignable to T. So let's just forget about T and use U[K][number] instead:

function mapToCreateObjects<
  K extends keyof U,
  U extends Record<K, any[]>
>(obj: U, key: K,): Record<K, { create: U[K][number] }[]> {
  return {
    [key]: obj[key].map((item) => ({ create: item })),
  } as any
}

Here I had to write something in place of T inside the constraint for U, so I just used the any type. There are usually ways to write code and completely avoid any, so feel free to do that if it matters to you.

Anyway we can now verify that your code behaves as desired by inspecting the inferred type of `transformedFruits:

/* const transformedFruits: {
    vegetables: {
        create: number;
    }[];
    fruits: {
        create: string;
    }[];
} */

Looks good.

Playground link to code

本文标签: TypeScript inferring incorrect return type when transforming array elements into objectsStack Overflow