Lordbalmon
Lordbalmon

Reputation: 1714

Type restriction in props is not recognized in the function

Why is the following object extraction flagged as incorrect by the compiler? playground link

type SplitTextProps<T, N extends keyof T> = T[N] extends string
  ? {
      object: T
      getter: N
    }
  : never

const splitText = <T, N extends keyof T>({
  object,
  getter
}: SplitTextProps<T, N>) => {
  const value = object[getter]
  return value.split(' ')
}

interface Datatype {
  value: string
  id: number
}

const record: Datatype = { value: 'Green is good', id: 5 }
const result = splitText({ object: record, getter: 'value' })

console.log(result)

value.split(' ') is marked as incorrect despite the type definition on SplitTextProps. The exact error message is

Property 'split' does not exist on type 'T[N]'


Edit (Adding more info): In fact, the restriction actually works when trying to pass incorrect props, the compiler throws the following error when trying to use splitText({object: record, getter : 'id'})

Argument of type '{ object: Datatype; getter: string }' is not assignable to parameter type 'never'.

ā˜ļø because id prop of interface Datatype isn't a string. Only the happy path fails within splitText func šŸ˜”.

Upvotes: 3

Views: 113

Answers (1)

jcalz
jcalz

Reputation: 328282

The problem here is that the TypeScript compiler cannot generally follow "higher order" logical operations with as-yet-unspecified generic types. Inside the body of splitText(), the type SplitTextProps<T, N> is a conditional type that depends on generic type parameters T and N. When the compiler tries evaluates the type of object and getter, it takes the shortcut of widening the conditional type to the union of its true and false branches, so it becomes { object, getter } : { object: T, getter: N }. Then value is of type T[N], which is correct. But notably, any information about T[N] extends string has unfortunately been lost. So value.split() is an error.

On the other hand, callers of splitText() usually don't have this problem. When you call splitText({ object: record, getter: 'value' }), the compiler can infer that you are specifying T as DataType and N as "value". And therefore that the input is of type SplitTextProps<DataType, "value"> which can be evaluated directly to be { object: DataType, getter: "value" }, and it type checks. If you call it some with some unexpected input, you'll likely get SplitTextProps<SomeT, SomeN> evaluating to never, and you get the expected error.

In short: unresolved generic conditional types are hard to deal with, so callers using specific types tend to be happy, and implementers using generic types can sometimes be unhappy.


If you want to leave things as-is, and you're sure you've implemented things properly, you could just assert to the compiler that value is a string:

const splitText = <T, N extends keyof T>(
  { object, getter }: SplitTextProps<T, N>
) => {
  const value = object[getter] as string; // <-- assert
  return value.split(' '); // okay
}

Otherwise, you'd need to refactor to a form the compiler can evaluate more correctly. Personally, I would stay away from conditional types unless they are necessary to represent your operation. For the code example as given, I'd be inclined to write it this way:

type SplitTextProps<N extends PropertyKey> = {
  object: Record<N, string>, getter: N
}

const splitText = <N extends PropertyKey>(
  { object, getter }: SplitTextProps<N>
) => {
  const value = object[getter];
  return value.split(' '); // okay
}

Here, SplitTextProps only cares directly about the literal type of getter N, while your T has been replaced with Record<N, string>, using the Record<K, V> utility type. The compiler is able to understand that Record<K, V>[K] is assignable to V, even for generic K. So then value is seen of a type assignable to string, and you are allowed to call split() on it.

Of course whether or not you can refactor your conditional type to something else that the compiler understands depends strongly on the use case, so there are probably times when a type assertion is your easiest and most straightforward solution.

Playground link to code

Upvotes: 2

Related Questions