Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Difference in between similar type constraint and conditional type

I have written my own implementation of the build-in utility-type InstanceType:

type MyInstanceType<C extends abstract new (...args: any) => any>
  = C extends abstract new (...args: any) => infer R ? R : never

It is very close to how it is actually implemented:

type BuiltInInstanceType<C extends abstract new (...args: any) => any>
  = C extends abstract new (...args: any) => infer R ? R : any

the only difference is that I'm having last word never while actual implementation has there any

It results in a difference in how it treats any:

type MyWithAny = MyInstanceType<any> // unknown
type BuiltInWithAny = BuiltInInstanceType<any> // any

I assume that it means that it is somehow getting to the falsy branch of the conditional type

So the question is, how is this possible? Considering that conditional type is the same as type constraint and so anything that is falsy to the conditional type should also be falsy to type constraint and so should result in a type constraint failed error

And the second question, why MyWithAny is exactly unknown and not e.g. never?

UPD: based on the answer and discussion, this is how it's getting evaluated (more details in the accepted answer):

  1. Usage of any as an argument to the generic function when evaluating MyWithAny and BuiltInWithAny has completely disabled the type constraints. So MyInstanceType has been evaluated like it's
type MyInstanceType<C> = C extends abstract new (...args: any) => infer R ? R : never

and BuiltInInstanceType has been evaluated like it's

type BuiltInInstanceType<C> = C extends abstract new (...args: any) => infer R ? R : any

that makes falsy branches possible

  1. In case when TS cannot evaluate the branch of a conditional type, it evaluates it to a union type of both branches. So because any includes instances that may or may not satisfy the conditional type, the type of MyWithAny is becoming R | never and the type of BuiltInWithAny is becoming R | any
  2. In case when TS cannot resolve R it considers as unknown. So, the result type of MyWithAny is unknown | never that is unkonown, and the result type of BuiltInWithAny is unknown | any that is any
like image 216
Agat Avatar asked Mar 21 '26 04:03

Agat


1 Answers

Your example can be reduced to smth like this:

type WithNever = <T>(t: T) => T | never
type WithAny = <T>(t: T) => T | any

type NeverRetutn = ReturnType<WithNever>
type AnyRetutn = ReturnType<WithAny>

We are interested in T | never and T | any.

If TypeScript is unable to infer type of T, TS will assign unknown type.

See example:

type ReturnUnknown = ReturnType< <T>(t: T) => T> // unknown

What is never? (docs)

The never type is a subtype of, and assignable to, every type

In fact, you should treat never as empty union.

type Check = 1 | never // 1
type Check2 = unknown | never // unknown
type Check3 = any | never // any

As you might have noticed, never is assigned to every type.

Let's go back to your question. In fact, you are receiving union of types.

type MyWithAny = MyInstanceType<any> // R | never
type BuiltInWithAny = BuiltInInstanceType<any> // R | any

I hope, it is clear now, that R | never will be resolved as unknown, because R is unknown and never is assignable to unknown.

R | any is the same as unknown | any, and because any type is assignable to any you are getting just any.

But why you are receiving the union of types? It is clear that conditional type should return true or false branch, but not both. This is not how it works in TS.

Because you have provided any as an argument every branch is applied. I think it is expected, because I don't have enough arguments to say that only true branch should be applied, or only false branch.

Considering that conditional type is the same as type constraint and so anything that is falsy to the conditional type should also be falsy to type constraint and so should result in a type constraint failed error

Since any type is in fact any type (every type), why do you think false branch should be applied ? Keep in mind, you are working with types, not the values.

What is confusing to me about falsy branch is that how I understand it, it should always be evacuated to never, no matter what we put there

It can't be always evaluated to false branch. Such assumption is not correct.

Consider next example:

type CheckAny = any extends abstract new (...args: any) => any ? true : false

Above condition evaluates to true | false. And true | false union is just a built in boolean type.

I don't think typescript is not smart enough. TS just tells you that any type can extend constructor type but in the same time it can not extend.

like image 64
captain-yossarian Avatar answered Mar 23 '26 19:03

captain-yossarian