I am struggling to figure out the correct use case of Generic default type parameter
.
Below I created an example where I want my generic function to use generic default type parameter instead of type argument inference. However, it doesn't seem to work. I think I am not able to differentiate between the two concepts of typescript generics.
interface Person<K extends string | number | symbol = string> {
addPerson: (key: K) => void;
}
interface StandardObject {
[key: string]: string;
}
// sometimes I want keys to be string and sometimes string literal
declare function returnPerson<T extends StandardObject = StandardObject>(param: T): Person<keyof T>;
var obj = {
name: "subrato",
gender: "male"
}
const record = returnPerson(obj)
record.addPerson('nam'); // I don't need this error, I want to ignore it
// above line should not throw error but bcz of type argument inference it doesn't
// Why TS uses type argument inference here, why not generic default type parameter `StandardObject`?
const record2 = returnPerson<StandardObject>(obj);
// here TS doesn't yell
// since we explicit define object type argument whose key's would be string
// I don't want to be explicit to ignore the string literal type
record2.addPerson("nam");
const record3 = returnPerson<typeof obj>(obj);
// I want obj key should accept string literal constant only If I have given the type argument like in this record3 example.
record3.addPerson("nam")
General question: It is when TS uses generics default type parameter instead of type argument inference?
My requirement is that my generic function returnPerson
to accept an object whose keys types to be sometimes string and sometimes string literal.
CodePudding user response:
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).
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 ofobj
, so the return type isPersonAdder<"name" | "gender">
.a2
is created by supplying an exact type forT
, 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 witha1
, the return type isPersonAdder<"name" | "gender">
(inferred fromtypeof 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 beingPersonAdder<string>
.a4
is the same scenario as what's happening witha2
, except that the manual type supplied forT
isStringObject
this time, so the return type infers the keys ofStringObject
(which arestring
), resulting inPersonAdder<string>
.a5
uses a type assertion again, this time asserting thatobj
is typeany
.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 inPersonAdder<string | number | symbol>
.