TL;DR When you use a generic type parameter as a function parameter's type, the type information will be inferred from the type of the argument that is used in the position of that parameter.
Consider the following example — it's essentially the same as what's in your question, except that it includes an implementation of your function declaration and a couple of the names are changed, however, the important parts are the same (type generics).
TS Playground
type PersonAdder<K extends PropertyKey = string> = {
addPerson: (key: K) => void;
};
type StringObject = Record<string, string>;
function makePersonAdder <T extends StringObject = StringObject>(param: T): PersonAdder<keyof T> {
// Is the same as no default type parameter because inference always takes place:
// function makePersonAdder <T extends StringObject>(param: T): PersonAdder<keyof T> {
return {
addPerson (key) {
console.log(key);
},
};
};
// Use:
const obj = {
name: "subrato",
gender: "male",
};
const a1 = makePersonAdder(obj); // PersonAdder<"name" | "gender">
a1.addPerson('name'); // ok
a1.addPerson('gender'); // ok
a1.addPerson('nam'); /*
~~~~~
Argument of type '"nam"' is not assignable to parameter of type '"name" | "gender"'.(2345) */
const a2 = makePersonAdder<typeof obj>(obj); // PersonAdder<"name" | "gender"> (identical to a1, original keys are inferred)
a2.addPerson('nam');
// ~~~~~
// same error as with a1
const a3 = makePersonAdder(obj as StringObject); /* PersonAdder<string>
^^^^^^^^^^^^^^^
Use a type assertion so that the inferred keys are simply 'string' */
a3.addPerson('name'); // ok
a3.addPerson('gender'); // ok
a3.addPerson('nam'); // ok
a3.addPerson('any other string'); // ok
a3.addPerson(42); /*
~~
Argument of type 'number' is not assignable to parameter of type 'string'.(2345) */
const a4 = makePersonAdder<StringObject>(obj); // PersonAdder<string> (identical to a3)
a4.addPerson('any other string'); // ok
a4.addPerson(42); /*
~~
Argument of type 'number' is not assignable to parameter of type 'string'.(2345) */
const a5 = makePersonAdder(obj as any); // PersonAdder<string | number | symbol>
a5.addPerson('any other string'); // ok
a5.addPerson(42); // ok
In the function makePersonAdder, a generic type parameter T is used.
T is used as both a constraint for what can be provided as the argument for the parameter param (extends StringObject), and as a variable for holding the inferred type of the argument provided for the parameter param. Because the return type uses type information which is inferred from T (PersonAdder<keyof T>), the type of the argument value is always used in the inference when creating the return type.
In the code above, there are multiple example usages:
a1 infers the keys from the type of obj, so the return type is PersonAdder<"name" | "gender">.
a2 is created by supplying an exact type for T, so that no inference takes place from the argument provided. Instead, the argument that you provide must extend the type provided. In this case, they are the same type, so just like with a1, the return type is PersonAdder<"name" | "gender"> (inferred from typeof obj).
a3 uses a type assertion on the argument provided, so that the compiler infers from the asserted type rather than original parameter's type. This results in the return type being PersonAdder<string>.
a4 is the same scenario as what's happening with a2, except that the manual type supplied for T is StringObject this time, so the return type infers the keys of StringObject (which are string), resulting in PersonAdder<string>.
a5 uses a type assertion again, this time asserting that obj is type any. any is a special top type which you can think of as essentially allowing any type information. Because this type is so broad, there's nothing to restrict the inference, so all of the permissible types are used, resulting in PersonAdder<string | number | symbol>.
Tof a generic functionreturnPersonto be sometimes string or sometimes string literal based on the calling site of the function.