TypeScript Conditional Type complains Type not assignable

5.1k Views Asked by At

I am trying to understand how TypeScript conditional type works. Here is my code. There are type errors:

interface MyType {
  name: string;
}

const testFunc = <T extends MyType | string>(
  what: T
): T extends MyType ? MyType : string => {
  if (typeof what === 'object') {
    return what['name'];
  }
  return what;
};

What is the correct usage?

enter image description here

enter image description here

3

There are 3 best solutions below

0
On

I'd do it like this:

interface MyType {
  name: string;
}

const testFunc = <T>(what: T): T extends MyType ? MyType : string => {
    if (typeof what === 'object') {
        return what['name'];
    } 
    return what as any;

};

as any means "TypeScript, don’t complain about this type". The problem is that the narrowed type of what is not picked up by the conditional type, so the function cannot evaluate the condition and narrow the return type to what.

0
On

This answer is basically explaining @jcalz's comment with more words and code.

You understand the concept correctly. Unfortunately you hit a caveat in TS, it doesn't treat concrete type and generic type equally when it comes to narrowing down possibility via control flow analysis.

Ideally, your proposed usage should be valid. However TS doesn't support it yet.

For now we need to workaround, and this is what I usually do.

interface MyType {
  name: string;
}

const testFunc = <T extends MyType | string>(
  _what: T
): T extends MyType ? MyType : string => {
  // First thing to do once enter the fn body,
  // we manually cast to `any` type
  var what = _what as any;
  if (typeof what === 'object') {
    return what['name'];
  }
  return what;
};

Not perfect, I know. It's kinda like you implement an overloaded function, eventually you just got to work with any type. But since you already provide a perfect function interface to your consumer, it's fine to go a bit dirty at back stage.

1
On

The function TestFunc in your code is supposed to return string in every case. I think it is a kind of typo. Let's fix it and go on.

Later I came up with a more safe solution (I leave my old answer at the bottom). It is better to use overloading. In an overloading you describe the conditional logic and in the function you use union types.

interface MyType {
  name: string;
}

function testFunc<T extends MyType | string>(
  what: T
): T extends MyType ? string : MyType;

function testFunc(what: MyType | string): MyType | string {
  if (typeof what === 'object') {
    return what.name;
  }
  return { name: what };
}

The old answer:

interface MyType {
  name: string;
}

type TestFunc = <T extends MyType | string>(what: T) => T extends MyType ? string : MyType;

const testFunc: TestFunc = (what: any) => {
  if (typeof what === 'object') {
    return what.name;
  }
  return { name: what };
};

Or if you prefer to define the type inline:

interface MyType {
  name: string;
}

const testFunc: <T extends MyType | string>(what: T) =>
  T extends MyType ? string : MyType =
  (what: any) => {
    if (typeof what === 'object') {
      return what.name;
    }
    return { name: what };
  };

Typescript compiler will handle it like this:

const a1: MyType = testFunc({ name: 'foo' }); // Type 'string' is not assignable to type 'MyType'.

const a2: MyType = testFunc({ name: 1 }); // Error: Argument of type '{ name: number; }'
//                                is not assignable to parameter of type 'string | MyType'

const a3: string = testFunc({ name: 'foo' }); // Ok

const a4: string = testFunc('foo'); // Error: Type 'MyType' is not assignable to type 'string'.

const a5: MyType = testFunc('foo'); // Ok