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
AllKeysObjis just equal toAnObjectWithAllKeys. - Suppose also that
PermittedObjis the type{goodkey1: string, goodkey2: string, foobar: string}, which is indeed a subtype ofPick<AllKeysObj, GoodKeys>. - Then
keyof PermittedObjwould be the union type'goodkey1' | 'goodkey2' | 'foobar'. - Then suppose
InferredPermittedKeysErroris the type'foobar', which is allowed because it is a subtype of that union type. - Of course,
'foobar'does not extendGoodKeys.
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'
>();
