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 toAnObjectWithAllKeys
. - Suppose also that
PermittedObj
is the type{goodkey1: string, goodkey2: string, foobar: string}
, which is indeed a subtype ofPick<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 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'
>();