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?
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.
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.
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.
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) .
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.
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.
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:
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:
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With