#How to vary the return type based on parameter types

29 messages · Page 1 of 1 (latest)

paper crypt
#

I'm having issues following along with some of the examples in the official docs re: conditional types:

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

The docs contain this example:

interface IdLabel { id: number }
interface NameLabel { name: string }
type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}

// a is a NameLabel
let a = createLabel("typescript");

// b is an IddLabel
let b = createLabel(2.8);

So far, so good. The problem is if you actually try to implement the createLabel function, it doesn't typecheck:

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  return typeof (idOrName) == "number" ? { id: idOrName } : { name: idOrName };
}

The exact error: Type '{ id: number; } | { name: string; }' is not assignable to type 'NameOrId<T>'. Type '{ id: number; }' is not assignable to type 'NameOrId<T>'.

My question is: why doesn't this work, and what's the right way to resolve it?

FWIW, there is a Stack Overflow post (~3 years old) that sort of addresses this issue, but the solution relies on adding additional casts/overloads that honestly seem to be unnecessary:

https://stackoverflow.com/a/52818072/569124

midnight tundra
#

I prefer to use overloads

#
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(idOrName: string | number): IdLabel | NameLabel;
function createLabel(idOrName: string | number): IdLabel | NameLabel {
    // ...
}
#

you can better document multiple overloads too

midnight tundra
#

conditional return types just often involve generics, so that's where you encounter them the most.

paper crypt
#

Thanks. What’s the difference between a) the cast-less return statements that fail typecheck and b) the return value assignments in the original example that seem to apply a correctly narrowed type to the variable? I’m not an algebraist but it kinda seems like both cases require a similar inference?

midnight tundra
#

so while the latter type-checks, typescript compares the overloads with the implementation signature bivariantly which means you can lie to it

fierce ibexBOT
#
webstrand#8856

Preview:```ts
function foo<T extends string>(x: T): T
function foo(x: string): string {
return "string unrelated to the input"
}

const result = foo("bar")
// result type is "bar", but at runtime it is something else```

midnight tundra
#

it's a lot safer than casting to a conditional type with as any though

paper crypt
#

Makes sense in the case of overloads!

I wasn't very clear in my last question, but I was wondering why this works:

// `a` is typed to NameLabel
let a = createLabel("foo");

while the return statement in my broken function won't allow a concrete string type as T extends number ? IdLabel : StringLabel.

My hypothesis is that when you call a function, the conditional expression is evaluated to give you the most refined type. Whereas in the return statement case, the typechecker is unable to go in the opposite direction, which is to verify that a) T is a string, b) the value being returned is an NameLabel, and C) that is a valid case that satisfies the conditional type.

#

I think my takeaway from this is as follows:

If you have function Foo() that tries to vary its return type based on the parameter types, then conditional type annotations are for the caller's safety, but not really for the safety of Foo()'s implementer. This is because the implementer has to rely on casts, because the typechecker won't try to prove that the function body satisfies a conditional in the return type.

midnight tundra
#

so typescript can't use the type information gained by type-guards and narrowing to narrow the type of the generic T, even if it could work conditional types backwards

paper crypt
#

Rad, thanks for walking me through it

paper crypt
midnight tundra
#

yeah, there's basically no way to get OneOf<string | number | whatever>

paper crypt
midnight tundra
#

yep

paper crypt
#

This is good to know on a practical level. On a theoretical level, I'm still not sure if I quite understand it. I feel like I, as a human, can prove the safety of my broken implementation. But is that actually true?

In other words, does the typechecker fail here because they just didn't implement the necessary parts (for perf or other reasons), or is it not actually correct on a math level to allow the typechecker to pass?

midnight tundra
#

right now types themselves never get narrowed only variables do

#

they'd have to add extra syntax if they were to change that, because you'd need a way to reference the unnarrowed type and narrowed type

paper crypt
#

Makes sense. Thanks again for the detailed response

#

!resolved