In the realm of TypeScript, there exist two distinct categories of generics.
Firstly, we have generic types, where the parameter is defined in the name of the type:
type GenTypeAlias<T> = T | number;
interface GenInterface<T> { prop: T }
class GenClass<T> { prop: T[] = [] }
To reference a value of this type, one must specify the type argument:
type SpecificTypeAlias = GenTypeAlias<string>; // valid
interface SpecificInterface extends GenInterface<number> { }; // valid
class SpecificClass extends GenClass<boolean> { }; // valid
type Oops = GenTypeAlias; // error!
// -------> ~~~~~~~~~~~~
// Generic type 'GenTypeAlias' requires 1 type argument.
For generic types, it is the responsibility of the individual providing the value to supply the type arguments.
On the flip side, there are generic functions, where the type parameter is declared within the call signature:
declare function genFunc<T>(val: T): T[];
type GenFunction = <T>(val: T) => T[];
Here, the type parameter does not need to be specified when referring to this type, but rather only during the function invocation:
const genFunc2 = genFunc; // no need to specify here
declare const alsoGenFunc: GenFunction; // no need to specify here
const strArr = genFunc<string>("abc"); // specify here
// const strArr: string[]
const numArr = genFunc<number>(123); // specify here
// const numArr: number[]
While the compiler can usually infer the type argument for a generic function call, it's still essential to specify it at call time.
const boolArr = genFunc(true);
// function genFunc<boolean>(val: boolean): boolean[]
// const boolArr: boolean[]
For generic functions, it is up to the consumer of the value to define the type arguments.
Now, focusing on your DataGenerator
definition:
type DataGenerator<T> = <T>(props: { name: string }) => T;
// -------------> ^^^
// 'T' is declared but its value is never read.
This seems to be both a generic function and a generic type, using the same type parameter name, T
. This leads to confusion due to the shadowing effect, where the innermost T
overrides the outer one.
type DataGenerator<T> = <T>(props: { name: string }) => T;
// ^-🛑 ^--------- refers to ----------^
To avoid this ambiguity, consider renaming the type parameters for clarity:
type DataGenerator<T> = <U>(props: { name: string }) => U;
By doing so, you explicitly indicate which type is being returned. With this revised approach, a DataGenerator<T>
will yield a result based on the chosen U
, offering more practicality in usage.
In summary, ensure that the implementer determines the type argument in such scenarios, leading to coherent functionality:
type DataGenerator<T> = (props: { name: string }) => T;
This adjustment results in seamless interactions with the DataGenerator
interface:
const dataGenerator: DataGenerator<{ city?: string }> = ({ name }) => {
return { city: `${name}` }
} // successful assignment
const result = dataGenerator({ name: "xxx" });
// const result: { city?: string }
Explore the nuances further in the provided Playground link for code exploration.