Xen_mar
Xen_mar

Reputation: 9752

How does the never type work in Typescript?

I often stumble on the never type in TypeScript. While I've read the docs, looked through existing questions on SO - my understanding still isn't great. I'd love some help understanding it better. Here's an example from the TS docs that has my head spinning:

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;

type Num = GetReturnType<() => number>;

type Str = GetReturnType<(x: string) => string>;

type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;

My understanding would be that specifying (...args: never[]) => ... would mean creating a function signature that does not accept any arguments (why never[]?). Yet Str and Bools are both function signatures with arguments (shouldn't this error?).

I'm also struggling to understand the conditional type: infer Return ? Return : never. To me this means that the function either has a return value or the compiler should produce an error. But I'm not sure.

Upvotes: 12

Views: 8462

Answers (2)

kaya3
kaya3

Reputation: 51162

never is a bottom type. What that means is that never is a type with no values, and it is a subtype of every other type. If you imagine that a type corresponds to a set of values which are assignable to that type, then never corresponds to the empty set.

My understanding would be that specifying (...args: never[]) => ... would mean creating a function signature that does not accept any arguments (why never[]?).

Technically you are almost correct; an array of type never[] cannot contain any values, but an empty array is assignable to type never[], because all zero of its elements are of type never. So technically a function of type (...args: never[]) => ... can be called with zero arguments.

That said, that's not really what's going on here. To understand T extends (...args: never[]) => infer R, we need to talk about covariance and contravariance. For the sake of explanation, let's suppose that we have two types, Animal and Dog, and Dog is a subtype of Animal:

  • First consider the function types () => Animal and () => Dog, which we'll call GetAnimal and GetDog respectively. A GetAnimal is a function which can be called to get an Animal; a GetDog can be called to get an Animal, because a Dog is an Animal, so therefore (by the Liskov substitution principle) GetDog is a subtype of GetAnimal. This means function types are covariant with their return types, because if you replace a function's return type with a subtype, then you get a subtype of the original function type. (And vice versa, if you replace the return type with a supertype, then you get a supertype of the original function type.)

  • Now consider the types (a: Animal) => void and (a: Dog) => void, which we'll call TakeAnimal and TakeDog respectively. A TakeDog is a function which cannot necessarily accept any Animal, so you cannot use a TakeDog anywhere a TakeAnimal is required; this means TakeDog is not a subtype of TakeAnimal. On the other hand, a TakeAnimal is a function which can accept any type of animal, including a Dog, so it is a function which accepts a Dog, and therefore (by the LSP again) TakeAnimal is a subtype of TakeDog. This means function types are contravariant with their parameter types, because if you replace a function's parameter type with a supertype, then you get a subtype of the original function type. (And vice versa, if you replace the parameter type with a subtype, then you get a supertype of the original function type.)

What this boils down to is that the function type (...args: never[]) => R is a supertype of all other function types which return R, because never[] is a subtype of every array type (because never is a subtype of every type, and array types are covariant with their component type), and it appears in contravariant position. So T extends (...args: never[]) => ... really means T can have any parameter types at all.

Personally I think this is a bad way of writing GetReturnType; it is more normal to write T extends (...args: any) => ... or T extends (...args: any[]) => ... because this is easier to understand, and also works. Note that Typescript has a built in helper type ReturnType which does the same thing as GetReturnType, and is defined this way:

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

In this definition, the type parameter T has (...args: any) => any as its upper bound, so you cannot use this to try to get the return type of something that isn't a function. Also, this means the extends clause of the conditional type is guaranteed to be satisfied, so the "false" branch (which was never in the definition you found) is guaranteed not to be used, so it doesn't matter what type is there (here it's any, but I think it would be clearer to write never).

I'm also struggling to understand the conditional type: infer Return ? Return : never. To me this means that the function either has a return value or the compiler should produce an error. But I'm not sure.

As mentioned in the other answer, you've parsed this wrong; the whole extends clause is the condition, so this should be read as (T extends (...args: never[]) => infer R) ? R : never.

More pertinently, this doesn't mean the compiler should produce an error when the function has no return type; it means the result should be the type never, which does not result in an error, and is arguably the correct result. Think of it like this: if a value is assignable to ReturnTypeOf<T> then it should be possible for something of type T to return that value when you call it. If T isn't a function then it can't return any value because you can't call it, so ReturnType<T> should be an empty set.

Upvotes: 25

Alex Wayne
Alex Wayne

Reputation: 187302

Usually never means "this type, in this scope, cannot be used". This is useful when instantiating generic parameters that you do not need to know the type of. In this case, to get the return value of a function, its arguments do not matter. So they can safely be typed as never.

So never[] here means "an array of some type, and please consider it a type error to assume it's any specific type".

This is preferable to any[] in this case because any lets you do anything with that type, and usually its use means a loss of type safety.

It's not so useful outside of generic type aliases. If you tried to use it more directly:

type Fn = (...args: never[]) => string
const fn: Fn = (a: number, b: string, c: boolean) => a.toFixed()
fn(123) // Argument of type 'number' is not assignable to parameter of type 'never'.(2345)

It allows the assignment for the same reason that GetReturnType works, but when you try to use or reference a never, you'll get a type error.


The second part of question is a completely different thing. First, you have the logic grouping incorrect. It's more like:

(Type extends (...args: never[]) => infer Return) ? Return : never

Or in psuedo code:

if (Type is subtype of ((...args: never[]) => infer Return) {
  use type Return
} else {
  use type never
}

infer lets you pull a deeply nested type from a larger type, but it only works inside a A extends B style ternary. When you see infer, think of the type as a pattern with a wildcard. Whatever that wildcard will be inferred as the type of whatever name you provide, and then that will be available in the positive side of the ternary.

A simpler example:

type HasCoolness<T extends { [key: string]: unknown }> = T extends { coolness: infer R } ? R : never
type TestA = HasCoolness<{ coolness: number }> // number
type TestB = HasCoolness<{ coolness: string }> // string
type TestC = HasCoolness<{ notCool: 'ugh' }> // never

That HasCoolness type infers the value of the coolness property and returns its type. If there is no property, never is returned indicating that the returned type is not valid for use.

So that all means this is doing:

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never

the same thing, except instead of inferring a property it's inferring the return type of a function. Note that because Type is not constrained (i.e. its not GetReturnType<Type extends SomeOtherType>) then you could pass anything to it. But if you pass something that isn't a function, it wont fir the pattern, and the negative clause of the ternary will be used, which will result in never.

Playground

Upvotes: 8

Related Questions