Wix
Wix

Reputation: 2214

Tyrying to do a keyof but restricting valid properties to string types

I have run into a little issue with Typescript. I am trying to write some functions to make dealing with my Redux state simpler.

I have this interface that I use to create some Entity State:

export interface NormalizedState<T> {
  byId: { [id: string]: T };
  allIds: string[];
}

I have some MailState like this:

export interface MailState extends NormalizedState<Mail> {
  inbox: string[]
}

And I have a function to add to the byId property like this:

type StringPropertyNames<T> = { [K in keyof T]: T[K] extends string ? K : never }[keyof T];

const appendToState = <T, K extends StringPropertyNames<T>>(s: NormalizedState<T>) => (v: T[], p: K) => 
  v.reduce((acc, m) => ({ ...acc, [m[p]]: m }), s.byId);

Problem is I get an error in the appendToState function at

[m[p]]

I get the following error:

A computed property name must be of type 'string', 'number', 'symbol', or 'any'.

So it seems like something in my typings is off. Any help would be greatly appreciated!

Upvotes: 2

Views: 525

Answers (1)

Titian Cernicova-Dragomir
Titian Cernicova-Dragomir

Reputation: 249536

Your types are fine, and for the caller they should work out ok. The problem is that inside a function Typescript can't really do a lot with conditional types. So it will not try to figure out that T[K] would work out to a string, it just sees the conditional and gives up.

The simple solution is to use a type assertion.

const appendToState = <T, K extends StringPropertyNames<T>>(s: NormalizedState<T>) => (v: T[], p: K) => 
  v.reduce((acc, m) => ({ ...acc, [m[p] as string]: m }), s.byId);

The other solution is to reverse the way you express the condition. Instead of K is any key of T that has the type string, we can say T must have a property K of type string. This alternate way is easier for the compiler to reason about

const appendToState2 = <T extends Record<K, string>, K extends PropertyKey>(s: NormalizedState<T>) => (v: T[], p: K) => 
  v.reduce((acc, m) => ({ ...acc, [m[p]]: m }), s.byId);

Upvotes: 4

Related Questions