Reputation: 30605
I am writing a function that runs through a list of parameters looking for elements. If found, the function strips the name and returns the value, and if not there, a default. The values will be either a number
or string
. I would like a way to use the typescript generics, and the constraints to get the following to compile.
function GetValue<T extends number | string>(datum: string, element: string, defaultValue: T): T {
if (datum.startsWith(element)) {
if (typeof defaultValue === 'number')
return parseInt(datum.substring(element.length)); // error
else if (typeof defaultValue === 'string')
return datum.substring(element.length); // error
}
return defaultValue;
}
I've excluded any error checking code, e.g. from the parseInt
. This would be called as;
console.log(GetValue("abc123", "abc", 456));
console.log(GetValue("abc123", "def", 456));
And expect to get out 123
and 456
.
Currently, it fails on the lines // error
since the function could be called with a type that is extends a string
or number
but is not a string
or number
.
How do I constrain the generic T
such that it is limited to be either a string
or number
(and not the union string | number
) and the return type is then matched to the type of the defaultValue
?
Upvotes: 9
Views: 6808
Reputation: 1575
Cast explicit to T, to remove this error
function GetValue<T extends number | string>(datum: string, element: string, defaultValue: T): T {
if (datum.startsWith(element)) {
if (typeof defaultValue === 'number')
return parseInt(datum.substring(element.length)) as T;
else if (typeof defaultValue === 'string')
return datum.substring(element.length) as T;
}
return defaultValue;
}
There is an issue open on github which discusses this topic.
Upvotes: 9
Reputation: 33041
Please take a look at other approach:
interface Overloading {
<T extends number>(datum: string, element: string, defaultValue: T): number;
<T extends string>(datum: string, element: string, defaultValue: T): string;
}
const GetValue: Overloading = <T extends number | string>(datum: string, element: string, defaultValue: T) => {
if (datum.startsWith(element)) {
if (typeof defaultValue === 'number')
return parseInt(datum.substring(element.length));
else if (typeof defaultValue === 'string')
return datum.substring(element.length);
}
return defaultValue;
}
const res = GetValue('12', 'sdf', 2) // number
const res = GetValue('12', 'sdf', 'sdf') // string
const res = GetValue('12', 'sdf', ()=>{}) // error
Sometimes it is better to not define explicitly return type of function. Let TS do it for you.
I have used this function overloading for constraints.
UPDATE
TS docs: function overloads
//Let's say you have an enum:
const enum State {
idle = 'idle',
active = 'active',
disable = 'disable'
}
//And you have a function:
// function handleState(state: State, payload: string | number | number[]): void;
// But you have next constraints:
// If state is `active`, payload should be only string
// If state is `idle`, payload should be number
// If state is `disable`, payload shoul be array of numbers number[]
// So let's update our handleState function
function handleState(state: State.active, payload: string): void;
function handleState(state: State.idle, payload: number): void;
function handleState(state: State.disable, payload: number[]): void;
function handleState(state: State, payload: string | number | number[]) {}
handleState(State.active, 2) // Error, active state can't be along with number payload
handleState(State.active, '2') // Ok, active state can be along with string payload
To make overloads for arrow functions, please consider interfaces, just like in my first example.
You can add to overloadings generic parameters, like I did.
Is that what you are asking about?
Because in case with generics:
interface Overloading {
<T extends number>(datum: string, element: string, defaultValue: T): number
<T extends string>(datum: string, element: string, defaultValue: T): string
}
T is either number or string, but not the union one.
Upvotes: 2