BenShelton
BenShelton

Reputation: 911

How do I infer to TypeScript that a property can only be of one type based on filtered keys?

I am trying to find a way to build a function that takes in a state, only allows specifying a property name based on a type (in this case string) and then doing something with that property.

In the following case everything works well except within the body of the function. The line return prop.substr(0) + payload errors because TypeScript cannot infer by this point that state[property] could only ever refer to a property that is of type string.

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

const state = { one: 'string', two: 34, three: true }

type StateStringKeys = StringKeys<typeof state> // ✓ should only contain 'one'

const combineStrings = <S, P extends StringKeys<S>>(state: S, property: P, payload: S[P]): string => {
  const prop = state[property]
  return prop.substr(0) + payload // X - How do I tell TS that this could only ever be a string by now?
}
combineStrings(state, 'one', 'test') // ✓ should NOT error, 'one' is a string and a string is provided
combineStrings(state, 'one', 23) // ✓ should error, 'one' is a string but a number is provided
combineStrings(state, 'two', 23) // ✓ should error, 'two' is a number
combineStrings(state, 'three', 'test') // ✓ should error, 'three' is a boolean
combineStrings(state, 'four', false) // ✓ should error, 'four' does not exist

Is there any way in this to have the same type safety when calling the function but also infer the correct type of string for prop.

Here's a link on TypeScript Playground if you want.

Upvotes: 0

Views: 39

Answers (1)

Maciej Sikora
Maciej Sikora

Reputation: 20132

The fix is to revert the constrain at the type level, force object to extend object with string values. Consider:

const combineStrings =
  < S extends Record<P, string> // here we say that object has P property as string
  , P extends keyof S >(state: S, property: P, payload: S[P]): string => {
  const prop = state[property]
  return prop.substr(0) + payload // works 👍
}

Playground

Upvotes: 1

Related Questions