Home > OS >  Two equivalent generic types are treated differently in function body
Two equivalent generic types are treated differently in function body

Time:11-22

I have a type that accepts type T as generic argument.

In a generic function's type arguments I derive T from an object's keys using Pick.

If I use an intermediate type variable to get these keys, it fails to act as T in the function body. If I skip the intermediate variable (everything else is the same) it acts like T!

Well annotated example in playground (this is the shortest I could make it)

Reproduced below:

//util to inspect types
type Id<T> = T extends object ? {} & { [P in keyof T]: Id<T[P]> } : T;
//-----

type AllKeys = "goodkey1" | "goodkey2" | "badkey";
type GoodKeys = "goodkey1" | "goodkey2"; //no badkey

//generic type only accepts good keys
type GenericType<T extends GoodKeys> = {
  something: T
};

//an object type with ALL keys (good and bad)
type AnObjectWithAllKeys = { [key in AllKeys]: any };

export function genericFunction<
  AllKeysObj extends AnObjectWithAllKeys,
  //an object with only good keys:
  PermittedObj extends Pick<AllKeysObj, GoodKeys>,
  //should only be good keys but errors as a type argument in function body
  InferredPermittedKeysError extends keyof PermittedObj,
  //exactly same as above only expanded (just no intermediate type)
  InferredPermittedKeysNOError extends keyof Pick<AllKeysObj, GoodKeys>,
>(
) {
  //why is this one giving an error string | number | symbol' is not assignable to type 'GoodKeys'
  type Err = GenericType<InferredPermittedKeysError>;
  //but the equivalent one does not??
  type NoErr = GenericType<InferredPermittedKeysNOError>;

  //return types to inspect
  return null as unknown as [InferredPermittedKeysError, InferredPermittedKeysNOError];
}

type Ret = ReturnType<typeof genericFunction>;

//hover over types and they are the same!
type ErrType = Id<Ret[0]>;
type NoErrType = Id<Ret[1]>;

What is going on here?

CodePudding user response:

The type InferredPermittedKeysError is defined as a subtype of keyof PermittedObj, and PermittedObj is any subtype of Pick<AllKeysObj, GoodKeys>, which means it can have arbitrary properties in addition to GoodKeys. So InferredPermittedKeysError could be an arbitrary property name, it does not have to be a subtype of GoodKeys.

For example:

  • Suppose AllKeysObj is just equal to AnObjectWithAllKeys.
  • Suppose also that PermittedObj is the type {goodkey1: string, goodkey2: string, foobar: string}, which is indeed a subtype of Pick<AllKeysObj, GoodKeys>.
  • Then keyof PermittedObj would be the union type 'goodkey1' | 'goodkey2' | 'foobar'.
  • Then suppose InferredPermittedKeysError is the type 'foobar', which is allowed because it is a subtype of that union type.
  • Of course, 'foobar' does not extend GoodKeys.

In contrast, InferredPermittedKeysNOError must indeed be a subtype of GoodKeys, because it is defined as a subtype of keyof Pick<AllKeysObj, GoodKeys> which is equal to the type GoodKeys. So in fact your two generic types are not equivalent; your Id function is giving the wrong result here.

To confirm, the following call is allowed, with no type errors:

// const test: ['foobar', 'goodkey1']
const test = genericFunction<
  AnObjectWithAllKeys,
  {goodkey1: string, goodkey2: string, foobar: string},
  'foobar',
  'goodkey1'
>();

Playground Link

  • Related