23

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

Matthieu Riegler
  • 31,918
  • 20
  • 95
  • 134
techguy2000
  • 4,861
  • 6
  • 32
  • 48
  • 2
    It's very difficult for the compiler to verify assignability to a conditional type, not to mention that [generic type parameters are not narrowed via control flow analysis](https://github.com/Microsoft/TypeScript/issues/13995). You'll likely want to use something like a [type assertion](https://www.typescriptlang.org/docs/handbook/basic-types.html#type-assertions) to suppress these errors. – jcalz Apr 12 '19 at 00:12

3 Answers3

11

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
Andrei Kovalev
  • 869
  • 7
  • 15
8

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.

hackape
  • 18,643
  • 2
  • 29
  • 57
-1

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.

Yakov Fain
  • 11,972
  • 5
  • 33
  • 38