0
votes

I have a function that can return a couple of different types and I want to specify the return using an argument:


type returnTypes = 'string' | 'object';

function getValue(returnType: returnTypes): string | Record<string, string> {
  if(returnType === 'object') {
    return { my: 'return value' }
  }
  return 'my return value';
}

interface IData {
  myObjectValue: Record<string, string>;
  myStringValue: string;
}
const data: IData = {
  myObjectValue: getValue('object'),
  myStringValue: getValue('string'),
}

TypeScript Playground

The assignment to data gets me the following errors:

Type 'string | Record<string, string>' is not assignable to type 'Record<string, string>'. Type 'string' is not assignable to type 'Record<string, string>'.

Type 'string | Record<string, string>' is not assignable to type 'string'. Type 'Record<string, string>' is not assignable to type 'string'

presumably because TypeScript thinks the return value of getValue is wider than the types defined in my interface.

How can I instruct TypeScript based on my returnType argument, which of the two return types I will receive?

(Or is there a more canonical way to do this, eg just separate into functions, if so what/how?

2
I don't think what you're trying to do is possible, because the typing system is purely a compile-time system, with no run-time effects. Testing what returnType is happens only at run-time.kshetline

2 Answers

2
votes

You can use overloads...

function getValue(returnType: 'object'): Record<string, string>
function getValue(returnType: 'string'): string
function getValue(returnType: 'string' | 'object'): string | Record<string, string> {
  if(returnType === 'object') {
    return {my: 'return value'}
  }
  return 'my return value';
}

interface IData {
  myObjectValue: Record<string, string>;
  myStringValue: string;
}
const data: IData = {
  myObjectValue: getValue('object'),
  myStringValue: getValue('string'),
}

Or a generic with conditional return type but will have to make assertions in the implementation while returning...

type returnTypes = 'string' | 'object';

function getValue<T extends returnTypes>(returnType: T): T extends 'string' ? string : Record<string, string> {
  if(returnType === 'object') {
    return {my: 'return value'} as any
  }
  return 'my return value' as any;
}

interface IData {
  myObjectValue: Record<string, string>;
  myStringValue: string;
}
const data: IData = {
  myObjectValue: getValue('object'),
  myStringValue: getValue('string'),
}
1
votes

The typescript compiler does not infer the returntype of the function by the value of parameters you are passing in. That would only be evaluated at runtime.

So it always assumes the returned type to be string | Record<string, string>. What you can do, is using a Type Assertion at the receiving site, to tell typescript, what type to expect.

type returnTypes = 'string' | 'object';

function getValue(returnType: returnTypes): string | Record<string, string> {
  if(returnType === 'object') {
    return { my: 'return value' }
  }
  return 'my return value';
}

interface IData {
  myObjectValue: Record<string, string>;
  myStringValue: string;
}
const data: IData = {
  myObjectValue: getValue('object') as Record<string, string>,
  myStringValue: getValue('string') as string,
}

Of course, such type asserations are not a guarantee, that this will work at runtime. Because if your getValue method has an error and you return a string even if the parameter is object, this will lead to follow up errors, when you try to access myObjectvalue.my

The "cleanest" solution of would probably be, to define two distinct functions, one returning a string the other one returning a Record<string, string>