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

Computed property key names should not be widened #13948

Open
sebald opened this issue Feb 8, 2017 · 34 comments
Open

Computed property key names should not be widened #13948

sebald opened this issue Feb 8, 2017 · 34 comments
Labels
Bug A bug in TypeScript
Milestone

Comments

@sebald
Copy link

sebald commented Feb 8, 2017

TypeScript Version: 2.1.5

Code
The latest @types/react (v15.0.6) use Pick<S,K> to correctly type the setState method of React.Components. While this makes it now possible to merge the state of a component instead of replacing it, it also makes it harder to write a dynamic update function that uses computed properties.

import * as React from 'react';

interface Person {
  name: string;
  age: number|undefined;
}

export default class PersonComponent extends React.Component<void, Person> {
  constructor(props:any) {
    super(props);

    this.state = { 
      name: '',
      age: undefined
    };
    this.handleUpdate = this.handleUpdate.bind(this);
  }

  handleUpdate (e:React.SyntheticEvent<HTMLInputElement>) {
    const key = e.currentTarget.name as keyof Person;
    const value = e.currentTarget.value;
    this.setState({ [key]: value }); // <-- Error
  }

  render() {
    return (
      <form>
        <input type="text" name="name" value={this.state.name} onChange={this.handleUpdate} />
        <input type="text" name="age" value={this.state.age} onChange={this.handleUpdate} />
      </form>
    );
  }
}

The above should show an actual use case of the issue, but it can be reduced to:

const key = 'name';
const value = 'Bob';
const o:Pick<Person, 'name'|'age'> = { [key]: value };

which will result in the same error. Link to the TS playground

Expected behavior:
No error, because key is a keyof Person, which will result in the literal type "name" | "age". Both values that are valid keys forstate.

Actual behavior:
The compiler will throw the following error:

[ts] Argument of type '{ [x: string]: string; }' is not assignable 
     to parameter of type 'Pick<Person, "name" | "age">'.
       Property 'name' is missing in type '{ [x: string]: string; }'.

My uninformed guess is that the constant key is (incorrectly) widened to string.

@aluanhaddad
Copy link
Contributor

My uninformed guess is that the constant key is (incorrectly) widened to string.

That is correct.
Note that by declaring the type of o to be Pick<Person, 'name'|'age'> you require that both name and age be present in the value.

@sebald
Copy link
Author

sebald commented Feb 9, 2017

Ok, so the above React example can never work, because setState requires explicit input since the function expects a Pick<S, K>? Which would mean that the issue is with the Typings and this is absolutely not the place to ask this question? 🙃

@DanielRosenwasser
Copy link
Member

@sebald You might mean something like

let peter: Pick<Partial<Person>, 'name' | 'age'> = {
    [key]: value
};

In any case, I don't think key should widen, so there looks like a bug here.

@DanielRosenwasser DanielRosenwasser added the Bug A bug in TypeScript label Feb 9, 2017
@DanielRosenwasser DanielRosenwasser changed the title Dynamic/computed keys with Pick<S,K> Computed property key names should not be widened Feb 9, 2017
@sebald
Copy link
Author

sebald commented Feb 9, 2017

@DanielRosenwasser thanks for the quick reply! I was wondering if using Partial is better to type setState.

For everyone that has the same issue: As a temporary fix you can write this.setState({ [key]: value });

@mhegazy
Copy link
Contributor

mhegazy commented May 11, 2017

This looks like a duplicate of #15534

@mhegazy mhegazy removed the Bug A bug in TypeScript label May 11, 2017
@mhegazy mhegazy added the Duplicate An existing issue was already created label May 11, 2017
@mhegazy mhegazy removed this from the TypeScript 2.4 milestone May 11, 2017
@AlexGalays
Copy link

@sebald

thanks for the quick reply! I was wondering if using Partial is better to type setState.

For everyone that has the same issue: As a temporary fix you can write this.setState({ [key]: value });

Partial wouldn't be ideal either because you could then update all non nullable properties with undefined.

Btw you posted a workaround that looks identical to the original code?

@mhegazy
Copy link
Contributor

mhegazy commented May 30, 2017

Automatically closing this issue for housekeeping purposes. The issue labels indicate that it is unactionable at the moment or has already been addressed.

@mhegazy mhegazy closed this as completed May 30, 2017
@mhegazy mhegazy reopened this Sep 22, 2017
@mhegazy
Copy link
Contributor

mhegazy commented Sep 22, 2017

with #18317 in the simple case with a single literal type works, but a union of literals still does not. we should distribute the union over the computed property.

@mhegazy mhegazy added Bug A bug in TypeScript and removed Duplicate An existing issue was already created labels Sep 22, 2017
@laverdet
Copy link
Contributor

laverdet commented Apr 3, 2020

The string literal change was a nice stopgap but definitely falls short. It would be nice to see this work with unique symbol types as well, since you actually must use computed keys.

const Key = Symbol();
type Thing = {} | {
	[Key]: string;
	property: string;
};

declare const thing: Thing;
const test1 = 'property' in thing ? thing['property'] : undefined; // this is fine
const test2 = Key in thing ? thing[Key] : undefined; // this is an error

@jcalz
Copy link
Contributor

jcalz commented Oct 23, 2020

cross-linking to #21030

@simeyla
Copy link

simeyla commented Jun 23, 2021

This is clearly a very old question, and a lot has changed.

You may find the following useful (feel free to skip to the accepted answer!)
https://stackoverflow.com/questions/68092396/dynamically-named-keys-in-typescript-that-wont-get-widened-to-key-string

@jcalz
Copy link
Contributor

jcalz commented Oct 11, 2021

Now that TypeScript supports pattern template literal index signatures, this widening to string is even more undesirable because there doesn't seem to be a way to use such template-pattern keys directly in an object literal:

function foo(str: string) {
  const key = `prefix_${str}` as const;
  // const key: `prefix_${string}`
  const obj = { [key]: 123 }
  // const obj: { [x: string]: number; } 😢
  // wanted const obj: { [x: `prefix_${string}]: number; }
}

Playground link

@biro456
Copy link

biro456 commented Apr 28, 2022

This makes computed properties error when used with mapped types too.

function partial<T, P extends keyof T>(field: P, value: T[P]): Partial<T> {
  return {
    [field]: value,
  };
  // Type '{ [x: string]: T[P]; }' is not assignable to type 'Partial<T>'.(2322)
}

Playground link

@jcalz
Copy link
Contributor

jcalz commented Dec 1, 2022

FWIW this is the helper function I tend to use when I need stronger types for computed keys:

function kv<K extends PropertyKey, V>(k: K, v: V): { [P in K]: { [Q in P]: V } }[K] {
	return { [k]: v } as any
}

Which produces these:

const test = kv("a", 123)
// const test: { a: number; }

const test2 = kv(Math.random() < 0.5 ? "x" : "y", "abc");
// const test2: { x: string; } | { y: string; }

const test3 = {
	a: 1,
	b: "two",
	...kv(Math.random() < 0.5 ? "a" : "b", true)
}
// const test3: { a: boolean; b: string; } | { b: boolean; a: number; }

function f(str: string) {
	const test4 = kv(`a${str}b`, new Date());
	// const test4: { [x: `a${string}b`]: Date; }
}

Playground link to code

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

No branches or pull requests