function createGenericCoordinates<Type extends number | string>(
x: Type,
y: Type
) {
return { x, y };
}
const genericCoordinates = createGenericCoordinates(1, 2);
// Error (TS2322)
// Type 3 is not assignable to type 1 | 2
genericCoordinates.x = 3;
How can I change this function so that the return type of both x and y is either number or string instead of the values I passed as x and y as argument functions (1|2 in this example)? If I remove „extends number | string“ it works, but the the allowed values for Type are not limited to number or string. Tested with TS 5.1.6
TypeScript uses various heuristic rules to determine whether a string or numeric literal like "a"
or 2
should be given a literal type like "a"
or 2
, or a corresponding widened type like string
or number
. These rules have been found to work well in a wide range of situations in real world code, but they don't always happen to agree with everyone's expectations.
Many of the rules in question were implemented in and described in microsoft/TypeScript#10676. In particular, "during type argument inference for a call expression the type inferred for a type parameter T
is widened to its widened literal type if [...] T
has no constraint or its constraint does not include primitive or literal types". So given the generic function
function createGenericCoordinates<T extends number | string>(
x: T, y: T
) { return { x, y }; }
the type parameter T
has a constraint which includes the primitive types string
and number
, so when the type argument for T
is inferred, it will not be widened.
This suggests that one approach to change this is to remove the constraint from T
and write the rest of your call signature so it enforces the intended constraint. Maybe like this:
function createGenericCoordinates<T>(
x: T & (string | number), y: T & (string | number)) {
return { x, y };
}
Now T
is unconstrained so T
will be widened when inferred. When you call createGenericCoordinates(x, y)
, the compiler will infer T
from the type of x
, but then check that x
is assignable to the intersection T & (string | number)
, and the same check will happen for y
. The only way that is possible is if T extends string | number
, so if either x
or y
are not compatible with string | number
, you'll get an error:
createGenericCoordinates(true, false); // error!
// --------------------> ~~~~
// function createGenericCoordinates<boolean>(x: never, y: never);
createGenericCoordinates({}, {}); // error!
// --------------------> ~~
// function createGenericCoordinates<{}>(x: string | number, y: string | number);
Since boolean & (string | number)
reduces to never
, the compiler rejects true
because it's not never
. Since {} & (string | number)
reduces to string | number
, he compiler rejects {}
because it's not string | number
.
If you call it with two strings or two numbers, there's no error, and the lack of widening gives you the results you wanted:
createGenericCoordinates(1, 2);
// function createGenericCoordinates<number>(x: number, y: number);
createGenericCoordinates("a", "b");
// function createGenericCoordinates<string>(x: string, y: string);