Niall
Niall

Reputation: 30605

How to limit a typescript generic to one of two primitives?

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

Answers (2)

Martin Godzina
Martin Godzina

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

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

Related Questions