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

Incorrect Type Inference When Using Dynamic Key with Union Value Types #60549

Open
oskarscholander opened this issue Nov 21, 2024 · 4 comments
Open

Comments

@oskarscholander
Copy link

πŸ”Ž Search Terms

Type inference with union types, Generics and union types, Keyof with union types

πŸ•— Version & Regression Information

Not really sure, started implementation of the feature in 5.6.3.

⏯ Playground Link

https://www.typescriptlang.org/play/?#code/KYDwDg9gTgLgBDAnmYcBqBLAJsCBxAQwFtgBnOAXjgG8AoOOAIiIwDtgBjKAgMxkYBcTMB0ZwAPk1IB3DDA4ALRgG56TIgSgYIAWgDWm-kMYy5ilWsZYMBAEYAbCIOGjVAX1W0cHe5tQ8AV1YOGG1WOABzYBhMHHxiYAAeAGk4UBhgVixyPWBECB50bFxCElIAPgAKNVYEoWSAGjUOCFZSCHtgIViShNIAbWSAXVoASiFSGC1WCM8Aejm4AFFwTgysIQA5CDSoKGg4SpZ2Ll54DHICADcCDF8HVFa4EVHaKJji+JJK5jZObj4jAaLkYo3mixWKBCwA2yz2ByOmm0+kMcAucFYEHg11u906cCeL1oCzh+ygcAI5FAUPWQgAglAIgESKx4AUEMhUAByRgiRhctHkTHY0ikDARWoPBA7MCaBIZcnspAoOA80zyJRct7RHpfYA-DRaXQGWBAkFg2heTi+KCoFpteBYRC1FgcUpdOC5fKFXXu0jguAASVFAQ9212ZMFz32WACHBhcAUwFtxIhq2hsKW8PJtk4BACpG5TpdGDdCQFLQC9iwcFz6iRxsMZukClLCijwopNzudnxT15om1Hzi7sqxeIpfdwIHoOUQA

πŸ’» Code

export type VideoGames = {
  "minecraft": "pc" | "switch";
  "mario-kart": "switch";
  "diablo": "pc";
};

declare function getVideoGame<K extends keyof VideoGames>(
  name: K,
  console: VideoGames[K]
): string;

// Expected: No error (minecraft is available on pc)
getVideoGame("minecraft", "pc");

// Expected: Error (mario-kart is not available on pc)
// Error as expected: Argument of type '"pc"' is not assignable to parameter of type '"switch"'
getVideoGame("mario-kart", "pc");

declare const dynamicGame: keyof VideoGames;

// Issue: No error is produced here
// Expected: Error because 'dynamicGame' could be "mario-kart", which is not available on "pc"
getVideoGame(dynamicGame, "pc");

πŸ™ Actual behavior

No error is reported on the last call. TypeScript allows getVideoGame(dynamicGame, "pc") without any type errors, even though "pc" is not a valid console for all possible games in VideoGames.

πŸ™‚ Expected behavior

An error should be reported on the last call to getVideoGame because dynamicGame can be any key of VideoGames, including "mario-kart", which does not support the "pc" console. The type system should enforce that the console argument is valid for all possible values of dynamicGame.

Additional information about the issue

This issue affects the type safety of functions that rely on generic keys with associated value types. It seems that when using a dynamic key of a union type, the compiler does not enforce that the provided value is valid for all possible keys, potentially allowing invalid combinations without type errors.

@MartinJohns
Copy link
Contributor

MartinJohns commented Nov 21, 2024

This is working as intended.

The type of keyof VideoGames is a union of all keys, so it's "minecraft" | "mario-kart" | "diablo". That means your call to getVideoGame has the type "minecraft" | "mario-kart" | "diablo" for the type argument K. And when a union for is fed to VideoGames[K] for K you end up with a union of all keys, so VideoGames["minecraft"] | VideoGames["mario-kart"] | VideoGames["diablo"], which resolves to "pc" | "switch", and your second argument "pc" is assignable to this.

As a workaround you can do this: (credit to y-nk)

type Kinded<T> = { [K in keyof T]: [key: K, val: T[K]] }[keyof T]
declare function getVideoGame(...args: Kinded<VideoGames>): string;

This forces you do provide matching literal types.

@MattisAbrahamsson
Copy link

MattisAbrahamsson commented Nov 21, 2024

This is working as intended.

The type of keyof VideoGames is a union of all keys, so it's "minecraft" | "mario-kart" | "diablo". That means your call to getVideoGame has the type "minecraft" | "mario-kart" | "diablo" for the type argument K. And when a union for is fed to VideoGames[K] for K you end up with a union of all keys, so VideoGames["minecraft"] | VideoGames["mario-kart"] | VideoGames["diablo"], which resolves to "pc" | "switch", and your second argument "pc" is assignable to this.

As a workaround you can do this: (credit to y-nk)

type Kinded = { [K in keyof T]: [key: K, val: T[K]] }[keyof T]
declare function getVideoGame(...args: Kinded): string;
This forces you do provide matching literal types.

Thank you for the workaround, we'll try it.

But it seems very strange that this should be intended behavior when it enables code that is not typesafe. In our getVideoGame function if we were to make a check like this

if (game === "mario-kart") {
// console is now inferred to "switch"
}

TypeScript now infers the type as expected, but it might be incorrect

Intended or not, this is a flaw in the type system that allows for unsafe code

@MartinJohns
Copy link
Contributor

if (game === "mario-kart") {
// console is now inferred to "switch"
}

That's not the case. The type will be VideoGames[K], aka unresolved. This assignment will fail: const val: "switch" = console;

@MattisAbrahamsson
Copy link

if (game === "mario-kart") {
// console is now inferred to "switch"
}

That's not the case. The type will be VideoGames[K], aka unresolved. This assignment will fail: const val: "switch" = console;

Oh you are absolutely right, I assumed it would infer it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants