2
votes

I'm not sure if this has been asked under different terminology. I've got a function that takes two arguments, where both can (independently) be a number, string, or undefined. The returned value is statically guaranteed to be also one of those two arguments:

export function wideNarrow(wide:number|string|undefined, 
                           narrow:number|string|undefined){
    return isNarrowScreen() ? narrow : wide;
}

In some part of my app I pass two numbers to the function:

wideNarrow(8, 0);

However, the inferred return type is string|number, which shows an error with a numerical operation:

const extendedAreaHeight = 26;
const baseY = extendedAreaHeight + wideNarrow(8, 0);

Operator '+' cannot be applied to types '26' and 'string | number'.ts(2365)

and

Object is possibly 'undefined'.ts(2532)

It can be statically inferred that this particular call will always return a number, why is TypeScript not detecting it and still thinking that a string or undefined might be returned, and how can I fix it without breaking existing functionality (e.g. I may return undefined or string, string or number, undefined or number and any combination of these in other places that I call the function)?

I'm on TypeScript 3.5.2/Vscode 1.36.1.

2
I don't know why it can't infer it that way, but it works if you define it as a generic: function wideNarrow<T extends number|string|undefined>(wide:T, narrow:T){ ... } - Thomas
@Thomas that seems like an answer to me, why don't you make it one. - Titian Cernicova-Dragomir
@Thomas nope, that doesn't work. it requires both arguments to be of the same type, which doesn't cover my use case. I need to be able to use it with a string and a number e.g. wideNarrow(25, '20%') - Can Poyrazoğlu
Then TS won't be able to infer the proper type. It doesn't look that deep into the code. Even if I do return true ? narrow : wide; the typescript playground does not infer from this that the returned type will always be typeof narrow; return type stays a union type typeof narrow | typeof wide - Thomas
well you can use a generic with two types function wideNarrow<T extends number|string, U extends number|string>(wide:T, narrow:U){ ... } but you'll still have the problem that the returned type is string|number when you use the function with a string and a number. - Thomas

2 Answers

3
votes

As Thomas suggested (and I simultaneously discovered), having two type arguments with the same signature solves the problem:

export function wideNarrow
  <T=number|string|undefined,
   K=number|string|undefined> 
   (wide:T, narrow:K){
  return isNarrowScreen() ? narrow : wide;
}

The reason for having two of exact types instead of one is that they can be different as I've stated in the question. If I made it just T without K and made both wide and narrow type of T, it forces both wide and narrow to be of same type (e.g. both numbers), whereas I sometimes need them to be of different type (e.g. wide string but narrow number).

Note: If anyone really wants to know in what scenario that I need it this way, I'm using it to fill stylesheets in my React Native app where some style elements are perfectly happy with both numbers e.g. (width: 100 of 100 points wide) or strings (e.g. width: '50%') for filling half of the container horizontally).

2
votes

You can add more precise function declarations, like this:

export function wideNarrow(wide: number, narrow: number): number;
export function wideNarrow(wide: number|string|undefined, narrow: number|string|undefined) {
    return isNarrowScreen() ? narrow : wide;
}

The function still has only one implementation, but multiple declarations, which helps the compiler. See Function Overloads documentation section.