Reputation: 9752
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
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 (whynever[]
?).
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
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
.
Upvotes: 8