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

Generic key name for object in callback function works only partially #40851

Closed
Svish opened this issue Sep 30, 2020 · 8 comments
Closed

Generic key name for object in callback function works only partially #40851

Svish opened this issue Sep 30, 2020 · 8 comments
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug

Comments

@Svish
Copy link

Svish commented Sep 30, 2020

TypeScript Version: 4.0.2

Search Terms:

generic known key extends string

Expected behavior:

I should be allowed to call fn with an object matching the expected type.

Actual behavior:

Getting the following error when trying to call fn, even though the object I pass in is matching the type.

Argument of type '{ [x: string]: string; }' is not assignable
to parameter of type '{ [key in Key]: string; }'.(2345)

The usage of the function shows that the callback, at least seemingly, is typed correctly. I.e. I am allowed to access obj.foobar, but not obj.somethingElse.

Related Issues:

I got a link to #13948 when asking about this on https://stackoverflow.com/q/64139075/39321, but I don't understand "Typescript lingo" or the more complicated React issue well enough to know if it's actually talking about the same issue as what I've run into here.

So I decided to post this as a bug. If it actually is the same, close this as a duplicate (and I'll know for sure), and if it's not, great, you have a new bug.

I'm guessing it might be reported before too though, but I really don't know what the correct name for this issue is, so I find it impossible to search for it. 😕

Code

function useObjectWithSingleNamedKey<Key extends string>(
  keyName: Key,
  value: string,
  fn: (obj: {[key in Key]: string}) => void)
{
  fn({ [keyName]: value }) // <-- Fails, but should not?
  fn({ [keyName]: value } as { [key in Key]: string }) // <-- Unwanted workaround
}

useObjectWithSingleNamedKey('foobar', 'test', obj => {
  console.log(obj.foobar)  // <-- Should work and does
  console.log(obj.notHere) // <-- Should fail and does
});
Output
"use strict";
function useObjectWithSingleNamedKey(keyName, value, fn) {
    fn({ [keyName]: value }); // <-- Fails, but should not?
    fn({ [keyName]: value }); // <-- Unwanted workaround
}
useObjectWithSingleNamedKey('foobar', 'test', obj => {
    console.log(obj.foobar); // <-- Should work and does
    console.log(obj.notHere); // <-- Should fail and does
});
Compiler Options
{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictPropertyInitialization": true,
    "strictBindCallApply": true,
    "noImplicitThis": true,
    "noImplicitReturns": true,
    "alwaysStrict": true,
    "esModuleInterop": true,
    "declaration": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "moduleResolution": 2,
    "target": "ES2017",
    "jsx": "React",
    "module": "ESNext"
  }
}

Playground Link: Provided

@RyanCavanaugh
Copy link
Member

This is a correct error, Key extends string doesn't mean you're only going to see one string. A legal invocation of your function could cause a crash:

useObjectWithSingleNamedKey<"foobar" | "bazbaz">('foobar', 'test', obj => {
  // Crashes
  console.log(obj.bazbaz.toUpperCase());
});

@RyanCavanaugh RyanCavanaugh added the Working as Intended The behavior described is the intended behavior; this is not a bug label Sep 30, 2020
@Svish
Copy link
Author

Svish commented Sep 30, 2020

@RyanCavanaugh Hm, ok, that makes sense I guess.

But, then, how do I do this correctly? Hopefully my intent is clear, but how can I make that intent clear to Typescript here?

@RyanCavanaugh
Copy link
Member

There isn't a way to express this (at least not without some trickery someone more clever than me would need to come up with). If you pinky promise to yourself not to call this with a union argument, I would just add a type assertion and call it a day.

@Svish
Copy link
Author

Svish commented Sep 30, 2020

Well, that's annoying. 😕

Guess this is a feature request then, rather than a bug report. Because I really feel like there should be some way to express my intent.

In my specific case, it shouldn't be a huge issue because the "function" is actually a React component with the following props:

interface InputProps<Name extends string> {
  name: Name;
  value: Date;
  onFieldChanged: (update: { [key in Name]: string; }) => void;
}

The goal was to have a typesafe "link" between the value of the name prop and the only key/value-pair which will exist in the in the update object passed to the onFieldChanged callback. As in the code I posted in the issue, the usage of it works as I expect it to. It's just the calling of the callback within the component that Typescript isn't happy with. 😕

<Input
  name="foobar"
  value={value}
  onFieldChanged={ update => setValue(update.foobar) }
  //              This foobar is typechecked ^^^^^^
/>

Manually specifying generic parameters of React-components isn't very common, so it's kind of an implicit pinky-promise, or what to call it. But yeah, it bugs me that there's no way to be properly explicit about this.

@RyanCavanaugh
Copy link
Member

See #27808

@Svish
Copy link
Author

Svish commented Sep 30, 2020

@RyanCavanaugh That talks about intersections of different types though. Not sure how it would solve this, since string is the only type in question. Unless it includes the possibility to do something like K extends oneOf(string).

@RyanCavanaugh
Copy link
Member

Yeah, presumably we'd allow K extends oneof string and allow returning a single-propertied object from that

@Svish
Copy link
Author

Svish commented Sep 30, 2020

Then I'll close this, as it would be solved by #27808 🙂👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Working as Intended The behavior described is the intended behavior; this is not a bug
Projects
None yet
Development

No branches or pull requests

2 participants