Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic type extending union is not narrowed by type guard

I tried to replicate Anders' example for conditional types and generics which he showed at Build 2018 (36:45). He uses a conditional type as a return type as replacement for more traditional function overloads.

The slide has the following:

type Name = { name: string };
type Id = { id: number };
type Check = { enabled: boolean };

type LabelForType<T> =
  T extends string ? Name :
  T extends number ? Id :
  T extends boolean ? Check :
  never;

declare function createLabel<T extends string | number | boolean>(value: T): LabelForType<T>

I tried to simplify this a bit and came up with the following example. The conditional type returns number when given a string and vice versa, while the function implements this conditional type as return type.

type Return<T> = T extends string ? number : T extends number ? string : never;

function typeSwitch<T extends string | number>(x: T):  Return<T>{
  if (typeof x == "string") {
    return 42;
  } else if (typeof x == "number") {
    return "Hello World!";
  }
  throw new Error("Invalid input"); // needed because TS return analysis doesn't currently factor in complete control flow analysis
}

const x = typeSwitch("qwerty"); // number

However both return statements show the same error:

Type '42' is not assignable to type 'Return<T>'.(2322)
Type '"Hello World!"' is not assignable to type 'Return<T>'.(2322)

What am I missing here?

like image 448
philmcole Avatar asked Mar 01 '20 13:03

philmcole


People also ask

What is type narrowing?

Type narrowing is the process of moving a type from a less precise type to a more precise type. Let's start with a simple function: function friends(input: string | number) { // code here } The above function can either take a number or a string.

Is type guard TypeScript?

A type guard is a TypeScript technique used to get information about the type of a variable, usually within a conditional block. Type guards are regular functions that return a boolean, taking a type and telling TypeScript if it can be narrowed down to something more specific.

How do you narrow down TypeScript?

The in operator narrowing JavaScript has an operator for determining if an object has a property with a name: the in operator. TypeScript takes this into account as a way to narrow down potential types. For example, with the code: "value" in x . where "value" is a string literal and x is a union type.

When should I use Typecript union type?

TypeScript - UnionTypeScript allows us to use more than one data type for a variable or a function parameter. This is called union type. Consider the following example of union type. In the above example, variable code is of union type, denoted using (string | number) .

Can We extend primitiveorconstructor in guardedtype and typeguard?

When we used T extends PrimitiveOrConstructor in both GuardedType and typeGuard, we saw that conditions about T 's type (e.g extending a constructor vs. extending keyof typeMap) didn't help the compiler narrow down T 's type, even though we defined PrimitiveOrConstructor to either be a constructor type or a valid property name of typeMap.

What is type guarding in C++?

Technique used inside of function print is known as type guarding. We first checked if parameter text is of type ‘string’. If the text is “string” that value is returned. Therefore in remaining part of the function it was known to compiler that parameter text is of type ‘string []’ – array of strings.

Is it possible to write a generic type guard for interfaces?

This means that writing a generic type guard to discern between interfaces wouldn't have worked - though one could write non-generic type guards for specific interfaces. This does work for classes, however:

How to use union types in typescript?

We can use union types in TypeScript to combine multiple types into one type: Variable text can now be either string or string array which can be pretty handy in some particular cases. Use case in a function: You can use union types with built-in types or your own classes / interfaces:


1 Answers

Here's why it doesn't work: Typescript does control-flow type narrowing on regular variables, but not on type variables like your T. The type guard typeof x === "string" can be used to narrow the variable x to type string, but it cannot narrow T to be string, and does not try.

This makes sense, because T could be the union type string | number even when x is a string, so it would be unsound to narrow T itself, or to narrow T's upper bound. In theory it would be sound to narrow T to something like "a type which extends string | number but whose intersection with string is not never", but that would add an awful lot of complexity to the type system for relatively little gain. There is no fully general way around this except to use type assertions; for example, in your code, return 42 as Return<T>;.

That said, in your use-case you don't need a generic function at all; you can just write two overload signatures:

// overload signatures
function typeSwitch(x: string): number;
function typeSwitch(x: number): string;
// implementation
function typeSwitch(x: string | number): string | number {
  if (typeof x === "string") {
    return 42;
  } else {
    // typeof x === "number" here
    return "Hello World!";
  }
}

Playground Link

like image 142
kaya3 Avatar answered Nov 04 '22 20:11

kaya3