How to have TS infer a callback in a generic builder function?

Viewed 200

Consider the following use case demo (playground):

// A builder that can self-reference its keys using a ref function
declare function makeObj<K extends string>(
    builder: (ref: (k: K) => number) => Record<K, number>
): Record<K, number>;

// Not using `ref` for now. All good, K is inferred as <"x" | "y">.
const obj1 = makeObj(() => ({ x: 1, y: 2 }));

// Oops, now that we try to use `ref`, K is inferred as <string>.
const obj2 = makeObj(ref => ({ x: 1, y: ref("invalid key, only x or y") }));

// This works, but we'd want K to be automatically inferred.
const obj3 = makeObj<"x" | "y">(ref => ({ x: 1, y: ref("x") }));

So, how should I write makeObj so K is automatically inferred?

2 Answers

Try declare Record<K, number> as second generic type.

playground

declare function makeObj<K extends string, T extends Record<K, number> = Record<K, number>>(
  builder: (ref: (k: K) => number) => T
): T

const obj1 = makeObj(() => ({ x: 1, y: 2 }));
const obj2 = makeObj(ref => ({ x: 1, y: ref("invalid key, only x or y") }));
const obj3 = makeObj(ref => ({ x: 1, y: ref("x") }));

Limitation

Well, as @kaya3 commented below. This resolution can only infer the return type. It still cannot find the invalid key unless explicitly set generic type.

// error will shown when given explicit generic type
const obj2 = makeObj<'x' | 'y'>(ref => ({x: 1, y: ref("invalid key, only x or y")}));
type Ref<Obj> = {
  ref<Key extends string & keyof Obj>
      (k:`${Key}`):Obj[Key]
};

declare function makeObject<Type>(obj: Type): Type & Ref<Type>;


const person = makeObject({
  firstName: "Saoirse",
  lastName: "Ronan",
  age: 26
});

person.ref('firstName') // (method) ref<"firstName">(k: "firstName"): string

// Argument of type '"firstNme"' is not assignable to 
// parameter of type '"firstName" | "lastName" | "age"'.ts(2345)
person.ref('firstNme') // ERROR

person.ref('age') // (method) ref<"age">(k: "age"): number

Reference "String Unions in Types"

Unfortunately you can't access ref when you are declaring the object in the call to makeObject. Fortunately, the native Javascript approximation is pretty good anyway.

Related