microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.13k stars 12.5k forks source link

Type Inference Fails with Generic Types #60447

Open maxnowack opened 1 week ago

maxnowack commented 1 week ago

πŸ”Ž Search Terms

error recursive generic types

πŸ•— Version & Regression Information

⏯ Playground Link

https://www.typescriptlang.org/play/?ts=5.6.3#code/C4TwDgpgBAQghgZwgSWBAtgHgCpQLxRwB2IAfPlAN5QCWAJgFxS4C+AUGzUWgE4BmcAMbQAykThgEACwD2wHFAgAPNEToJYiFGizJyBeElQYANFGQViZKmyi1GUBMB5cA5idtRgNdBCZEAV3QAIwgeDztBGQAbaIhBbxkiADk4XyYnFyJ3TxodBCZsAG0AXQ92NiiiJ0cIOISZHiYAMWi4YABFALCQTDEJaTlMQ20MUn0qKCjY+MSUtL8oAHJEOj4lqHYAei2oAD09th2oZJlFHh5GqEBQcg4+AKIEmiSvCCdMY3RscGhlVXVNEYdB86N9IKQzMhQT9LCRSAAKACUNjsoEg5h0AGkICANAQACJyU7AdrPIjY3F9cSSWTyT5giCQ6Hg0ieY4HNm7ABE9C5UAAPlAud5fHzBVzpvU5qlRQKhXkMAgxVAAAYK9AIAB0ABJKJk3CwVZ5PGjoABhGJSskyiAMiitdqqTAAcQg8n6NKG9J+TIZEOWktm1oWS3GnP2hzsxy5+uyXI4kSSNVcECIYRoghEdVmjRabU63R4vQ9gzpOgZvp+4wo1EDTySNqYKwQaw27Cjuw5HagAFELldADLkhRhS1rlqDDYWGWcbgA3JsNjQNEQ5IQEAgaK5xME4l4zqblg6Cz0qQNaR9yz7zMyIOMlpq2BVjgBVbzRPIgLww4BnQRSeIANa0HwhBfuiS6EEQhAXHAn5XDIwQAFazA+B7IAgADySGzDgEy4H8qYAghyEJFAAD8UDwvhKiERoACCMG9FY5AUQI0RIFATDON0yJMGxSAcMcABK8QBDwG4AG7QAeP5QCmaY8O00B0HIAC0K4knMUAATiCBsAehLAMSpJJBSCC4TWnhFJitBQTpIAyCBuAAGSUbGrhyoEIRhIiJQMJ4xSYiUig0Wo9GMZgXB8GEUDPqy3bICBeRLBoxDQYpIBmFwgjRAEdDQMEchSF4-62flSiQXQJXQNqUAAO40NEdCCHAPB0J4dgUSquqYoacrdZQvU6iq-U9SwOqUIZxlzGZmBxYa4aJbQwApZBUDEbMZg8KJ4nQFwsl5B1nHmJh2EJDg1klOQBFhV4PDdEdnWqmNI2CgNQ26lNcgmeSukXUFpALXYwPHe9fXHCMUxaEwABSAQ1NtwBiVBwClfZj5FPZjnMFArnwu5nlBKEPC+RwB4iGA77uiF-waO5Zj4nUPgKjwNO0Y4M7ZPongiGzt0DVFMUABIQHAdAsJ9TPoCzEuUILrPYHAjVA8DFFFCLYtmJq2sU1TOBK9EDNSyzpAlEdTBFCIZv6TCrrAAyMAgAACq1wDmdgZguzwbt8wC21i0k0Sfu5pTc3YXs+zdAJFPLUAa3QWva7HiuNWbIMUfHvsaFjTmPeRUB2w7zuu+76ui3QZS9ioinnSnhsc1krih-FIPHZnUcaEs2pLHnT3UbTUAMRlkVENFrNxb3qsF26RcR+Zz5mD21dCPIddmCHV0t63wP+BAUk8JPu-7+bzBk7bbo4J77TFR3DduGH0-2z8jtz5fUC63kmAu6jZj3qGrI23REeJ02A8JZ0giAUo+digAAZgqFDPkA-MXQTygMsnYIoTtbJQC+ppMks1QElDInmR0qYXQXw9lAJ2YZ2BAA

πŸ’» Code

type BaseItem<T = any> = { id: T }

interface Snapshot<T extends BaseItem<I> = BaseItem, I = any> {
  id: string,
  time: number,
  collectionName: string,
  items: T[],
}

const selector: FlatQuery<Snapshot<BaseItem>> = { collectionName: 'asdf' }
// ^^
// No error βœ…

function test<ItemType extends BaseItem<IdType>, IdType = any>() {
  type ItemKeys = DotNotationKeys<Snapshot<ItemType, IdType>>
  // ^^
  // "id" | "time" | "collectionName" | "items" | `items.${string}`

  type CollectionNameType = Flatten<Get<Snapshot<ItemType, IdType>, 'collectionName'>>
  // ^^
  // "string"

  const genericSelector: FlatQuery<Snapshot<ItemType, IdType>> = { collectionName: 'asdf' }
  // ^^
  // Error ❌: Type '{ collectionName: string; }' is not assignable to type 'FlatQuery<Snapshot<ItemType, IdType>>'.
}

// Utility type to check if a type is an array or object.
type IsObject<T> = T extends object ? (T extends Array<any> ? false : true) : false

// Recursive type to generate dot-notation keys
type DotNotationKeys<T> = {
  [K in keyof T & (string | number)]:
  T[K] extends Array<infer U>
  // If it's an array, include both the index and the $ wildcard
    ? `${K}` | `${K}.$` | `${K}.${DotNotationKeys<U>}`
  // If it's an object, recurse into it
    : IsObject<T[K]> extends true
      ? `${K}` | `${K}.${DotNotationKeys<T[K]>}`
      : `${K}` // Base case: Just return the key
}[keyof T & (string | number)]

type Split<S extends string, Delimiter extends string> =
  S extends `${infer Head}${Delimiter}${infer Tail}`
    ? [Head, ...Split<Tail, Delimiter>]
    : [S]

type GetTypeByParts<T, Parts extends readonly string[]> =
  Parts extends [infer Head, ...infer Tail]
    ? Head extends keyof T
      ? GetTypeByParts<T[Head], Extract<Tail, string[]>>
      : Head extends '$'
        ? T extends Array<infer U>
          ? GetTypeByParts<U, Extract<Tail, string[]>>
          : never
        : never
    : T

type Get<T, Path extends string> =
  GetTypeByParts<T, Split<Path, '.'>>

type Flatten<T> = T extends any[] ? T[0] : T

type FlatQuery<T> = {
  [P in DotNotationKeys<T>]?: Flatten<Get<T, P>>
}

πŸ™ Actual behavior

Typescript isn't able to assign { collectionName: 'asdf' } when using generic types:

function test<ItemType extends BaseItem<IdType>, IdType = any>() {
  const selector: FlatQuery<Snapshot<ItemType, IdType>> = { collectionName: 'asdf' }
  // ^^
  // Type '{ collectionName: string; }' is not assignable to type 'FlatQuery<Snapshot<ItemType, IdType>>'.
}

Without generic types it work seamlessly:

const selector: FlatQuery<Snapshot<BaseItem>> = { collectionName: 'asdf' }

πŸ™‚ Expected behavior

{ collectionName: 'asdf' } should be successfully assignable to FlatQuery<Snapshot<ItemType, IdType>> using generic types.

Additional information about the issue

Related issue: https://github.com/maxnowack/signaldb/pull/1030

RyanCavanaugh commented 1 week ago

We really need a less (much less) complex repro in order to have any chance of investigating this before other issues which have much more straightforward repros. Please try to get this down to a minimal sample if you'd like us to look at it. Thanks!

maxnowack commented 1 week ago

Okay. Iβ€˜ll try to strip it down as much as possible and update the issue

mkantor commented 1 week ago

I'm not the person who opened this issue, but ended up looking at it and I think the core problem essentially boils down to this:

type Example<T extends 'b' | 'c'> =
  Partial<{ a: unknown } & Record<T, unknown>>

<T extends 'b' | 'c'>() => {
  const problem: Example<T> = { a: null }
//      ^^^^^^^
// Type '{ a: null; }' is not assignable to type 'Partial<{ a: unknown; } & Record<T, unknown>>'.
}

Any instantiated Example<ConcreteType> will allow an object containing only the key a to be assigned to it, so maybe Example<T> (where T is a type parameter) should also allow it?