Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug Report: TS creates contravariant intersection when K extending keyof T assigned new type #54547

Closed
RoboZoom opened this issue Jun 6, 2023 · 7 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@RoboZoom
Copy link

RoboZoom commented Jun 6, 2023

Bug Report

🔎 Search Terms

T[K] assignment does not transform type of T

🕗 Version & Regression Information

Version: 4.8.4 to Present

This is bug became relevant when 4.8.4 released.

⏯ Playground Link

Playground link with relevant code

💻 Code

const joinSample = <T, K extends keyof T, U extends HasID>(
  parent: T,
  parentKey: K,
  children: U[],
) => {
  const keyArray = parent[parentKey] as string[];
  return {
    ...parent,
    [parentKey]: keyArray.map((k) => findOneById(children, k)) // Returns type U | undefined
  };
}

🙁 Actual behavior

The above code infers the output to joinSample as T & [x: string] : (U | undefined)[] which indicates that the T has the same key twice, with different types.

This is demonstrated in the playground when joined is typed to FullFoo. Untyped, when run, it produces an object that meets the criteria for FullFoo. When declared with type FullFoo, it raises an error.

🙂 Expected behavior

I would expect Typescript to substitute the type of parentKey from its previous value (in this case, always string[]) to (U | undefined)[]. The compiler knows that K is a keyof T - instead of creating a never condition, it should replace the type of T[K] to U.

@RoboZoom RoboZoom changed the title Bug Report: TS infers no transformation of T when T[K] assigned U Bug Report: TS creates contravariant intersection when K extending keyof T assigned new type Jun 6, 2023
@RyanCavanaugh RyanCavanaugh added the Needs More Info The issue still hasn't been fully clarified label Jun 7, 2023
@RyanCavanaugh
Copy link
Member

The Playground link is very very long, and the sample here has unbound identifiers. Can you please provide a minimal sample that more clearly demonstrates the problem you're describing?

@RoboZoom
Copy link
Author

RoboZoom commented Jun 7, 2023

This is as condensed as I can get it: Typescript Playground

Most of the space is scaffolding. The link above doesn't represent the desire to re-type the output in accordance with the generic input so that the object[K] has type U[], but I think it does represent the problem. Everything is still scaffolding to demonstrate the following:

const joinSample = <T, K extends keyof T, U extends HasID>(
  parent: T,
  parentKey: K,
) => {
  const keyArray = parent[parentKey] as string[];
  return {
    ...parent,
    [parentKey]: keyArray.map((k) => 123) // This assignment causes the problem
  };
}
//...
const joined: FullFoo = joinSample(sampleFoo, 'bars'); // Error - the type for joined is inferred as {id: string, bars: string[], bars: number[]}

The expectation is that when constructing the return object, it would understand the standard JS behavior that if you spread a parent, then add a specific child key, that it will overwrite the specific key if in the parent.

@RyanCavanaugh RyanCavanaugh removed the Needs More Info The issue still hasn't been fully clarified label Jun 7, 2023
@RyanCavanaugh
Copy link
Member

The problem here is that there isn't a correct type to describe what joinSample actually does, so TypeScript errs on the side of caution when describing it.

First let's fix joinSample to do what you want, then show why that's "wrong":

const joinSample = <T, K extends keyof T>(
  parent: T,
  parentKey: K,
) => {
  const keyArray = parent[parentKey] as string[];
  return {
    ...parent,
    [parentKey]: keyArray.map((k) => 123)
  } as T & Record<K, number[]>;
}

This behaves as desired.

However, if you invoked this as

const joined: FullFoo = joinSample<typeof sampleFoo, 'bars' | 'id'>(sampleFoo, 'bars');

then the implied behavior based on the type definition is that you'd have an object with bars: number[], id: number[], but that's obviously not what's happening -- the function can't (as in, can't possibly) runtime-enumerate all possible inhabitants of K to provide properties of it.

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Jun 7, 2023
@fatcerberus
Copy link

Chalk up another use case for #27808.

@RoboZoom
Copy link
Author

RoboZoom commented Jun 8, 2023

First, you if you're having to cast the output, then you aren't getting appropriate inference and I'd argue it's still a problem.

Second, your application of joinSample typing isn't an appropriate application of logical types. If you are looking at K as a union, then the output should be a union of each of the different possibilities driven by the input union.

So T has keys id and bars, then K is by definition id and bars, then in this case the output should reasonably be (and is certainly computable):

{ bars: number[]; id: string } | {bars: string[]; id: number[]}

This could be further narrowed when the function is called.

@fatcerberus
Copy link

@RoboZoom I think Ryan’s point is, ultimately, that there’s no way to represent that type operation generically (since your implementation is generic) using the type operators that are currently available. That would require spread types.

Basically there’s no type that TypeScript can currently infer that would accurately represent the output of this function.

@RyanCavanaugh
Copy link
Member

First, you if you're having to cast the output, then you aren't getting appropriate inference and I'd argue it's still a problem.

The point of that example is to show that the inferred signature is the more correct one. If we gave it the type you're saying it should have, the output would be wrong.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests

3 participants