1

Passing a object's property by name for reading can be done in a variety of ways. However, none of them seems to allow updating the property without explicit type conversions.

The function should be able to get and set the value of the property type-safe. The original use case was to edit the value; for simplicity, let's assume we just want to append a space.

All of these behave the same (get OK, set ERR):

  • { [k in K]: string } and keyof T

    function appendSpace1<
        T extends { [k in K]: string },
        K extends keyof T
    >(obj: T, path: K) {
      let x = obj[path]; // `x` is `T[K]`
      let y = x + " ";   // `y` is `string`
      obj[path] = y;     // Type 'string' is not assignable to type 'T[K]'.(2322)
      return x;
    }
    

    Playground link

  • Record<K, string> and string

    function appendSpace2<
        T extends Record<K, string>,
        K extends string
    >(obj: T, path: K) {
      let x = obj[path]; // `x` is `T[K]`
      let y = x + " ";   // `y` is `string`
      obj[path] = y;     // Type 'string' is not assignable to type 'T[K]'.(2322)
      return x;
    }
    

    Playground link

  • Record<K, string and KeysWithValsOfType<T, V>

    type KeysWithValsOfType<T, V> = keyof {
      [P in keyof T as T[P] extends V ? P : never]: P;
    };
    
    function appendSpace3<
        T extends { [k in K]: string },
        K extends KeysWithValsOfType<T, string>
    >(obj: T, path: K) {
      let x = obj[path]; // `x` is `T[K]`
      let y = x + " ";   // `y` is `string`
      obj[path] = y;     // Type 'string' is not assignable to type 'T[K]'.(2322)
      return x;
    }
    

    Playground link

These work (get and set OK), but…

  • Using a class

    This works when using a class, but it fails when transforming the code into type with generics:

    type MyClass = {
      a: number;
      b: number ;
      c: string ;
    }
    
    function increment<T>(obj: T,
         key: {
          [K in keyof T]-?: number extends T[K] ? K : never
        }[keyof T]
      ) {
        obj[key]++; // fail
      }
    
    const test: MyClass = {a: 0, b: 1, c: "two"};
    
    increment(test, "a");
    increment(test, "b");
    increment(test, "c"); // fail
    increment(test, "d"); // fail
    
  • Constant name

    function appendSpace4<
        T extends { x: string },
        K extends "x"
    >(obj: T, path: K) {
      let x = obj[path]; // `x` is `T[K]`
      let y = x + " ";   // `y` is `string`
      obj[path] = y;     // OK!
      return x;
    }
    

    Playground link

    … but it is not parameterizable. As soon as one of the xes is changed (first to [k in K] or second to keyof T), obj[path] = y turns back into ts2322.

I seem to be able to circle the solution very closely, but just not getting to it. Why is the set operation so different?

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Marcel Waldvogel
  • 422
  • 3
  • 10

0 Answers0