Home > Enterprise >  Type of argument depending on value of other one
Type of argument depending on value of other one

Time:07-01

  • The following function admit a generic called T.
  • The property key only admit a string that should be a key of T. We will use Obj as T.
  • The property value should admit the type of Obj[key]. Cant manage to implement it.
const fn = <T>({
  key,
  value
}: {
  key: keyof T;
  // Value type should be the type of the value of the key 'key' of 'T' generic
  value: typeof T[key];
}) => {
  return undefined;
};

type Obj = {
  keyA: {
    propertyFromA: string;
  };
  keyB: {
    propertyFromB: string;
  };
};

fn<Obj>({
  key: "keyA",
  value: { propertyFromB: "string" } // should only admit type { propertyFromA: "string" }, since Im passing 'keyA'
});

Live demo

CodePudding user response:

(Aside: I am renaming your types to conform with common TypeScript naming conventions: your Obj type will become T, to make it obvious that this is a generic type parameter and not a specific type; and, your obj type will become Obj, to make it obvious that this is a TypeScript type and not a JavaScript value. I suggest you make the same changes to the code in your question to make it easier for others.)

The type of value is something like T[typeof key]. But since you have typeof key as being just keyof T, then this is T[keyof T] and therefore a union of all possible property value types from T. Since you want the compiler to only allow value properties that correspond to the particular subtype of keyof T the caller uses for key, this indicates that you need the type of key to be an additional generic type parameter constrained to keyof T.

That gives us:

const fn = <T extends object, K extends keyof T>({
  key,
  value
}: {
  key: K;
  value: T[K];
}) => {
  return undefined;
};

But unfortunately now you can't call

fn<Obj>({ // error!
  key: "keyB",
  value: { propertyFromB: "string" } 
});

That's because TypeScript does not have partial type argument inference as requested in microsoft/TypeScript#26242. See this question and its answer for more detailed information.

When you call a generic function, you either need to manually specify all the type parameters, or not specify any of them and let the compiler infer them. You can't specify one and let the compiler infer the other. Which is sad, because that's exactly the behavior you want. For now there are only workarounds.


One workaround for this is to use currying to split the generic function into two, so callers can specify the type argument to the first function, which then returns another generic function where callers let the compiler infer the remaining type parameters. It looks like this:

const fn = <T extends object>() => <K extends keyof T>({
  key,
  value
}: {
  key: K;
  value: T[K];
}) => {
  return undefined;
};

So now instead of calling fn<Obj>({...}), you need to call fn<Obj>()({...}) (note the extra function call). If you're going to use fn<Obj>() a lot, you can save it to an intermediate function:

const fnObj = fn<Obj>();

And then use it to get the inference you want:

fnObj({
  key: "keyA",
  value: { propertyFromB: "string" } // error
});

fnObj({
  key: "keyB",
  value: { propertyFromB: "string" } // okay
});

Playground link to code

CodePudding user response:

I can't really explain why, so this may be a lacking answer, sorry for that (I will update the answer if I found or someone comments). But from my experience, I only managed to make what you want work by using automatic inference from the function arguments.

Type the function without default values for the types, and create the generics for the other fields, like key and value:

const fn = <Obj extends {}, K extends keyof Obj, V extends Obj[K]>(o: Obj, key: K, value: V) => {
  return undefined;
};

Now you can call the function with the three parameters, and the inference will work as expected:

type obj = {
  keyA: {
    propertyFromA: string;
  };
  keyB: {
    propertyFromB: string;
  };
};


fn({} as obj, "keyA", {propertyFromB: 'bar'}); //error
fn({} as obj, "keyA", {propertyFromA: 'foo'}); //success

You probably should use a real object instead of this cast: {} as obj

Here is a working playground

  • Related