1

When I use TypeScript's void type in conjunction with other types using intersection, I get different results.

type A = void & {} // A is void & {}
type B = void & '1' // B is never
type C = void & 1 // C is never
type D = void & string // D is never
type E = void & String // E is void & String
type A = void & {} 
type E = void & String

They should also be never types, right?

lumozx
  • 13
  • 2
  • How would you ever have, at runtime, a value of `void & {}` or `void & String`? – Alex Wayne Apr 10 '23 at 04:50
  • Intersecting `void` with a primitive produces `never`, since you can't have a primitive that also satisfies `void`. Interestingly, that does not seem to apply to intersections of `void` with objects... – kelsny Apr 10 '23 at 04:55
  • Oh yeah, and also, `void & undefined` is `undefined`, which makes a little sense but it's an exception to the void-intersected-with-primitive-rule. – kelsny Apr 10 '23 at 04:55
  • I believe that `void & undefined //-> undefined` is basically the same as `number & 123 //-> 123`. Since `undefined` is assignable to `void` without a problem `const a: void = undefined // fine`. Put another way, `undefined` extends `void`. – Alex Wayne Apr 10 '23 at 05:06
  • 1
    [`void` is *weird* and not really consistent](https://stackoverflow.com/a/69732504/2887218) so any confusing behavior can be chalked up to "‍♂️ `void`s will be `void`s ‍♂️". If `void` were a special flavor of `undefined` then `void & {}` or `void & string` should be `never`. But it's also a special flavor of `unknown` since you can assign `() => X` to `() => void` for all `X`, and then `void & {}` or `void & string` should probably stay as-is. So what explains the fact that `void & string` is `never` but `void & {}` stays as-is? "`void`s gonna `void`". – jcalz Apr 10 '23 at 14:29

1 Answers1

3

{} and String are both object types, and string and '1' are primitive types. You can intersect void with object types because object types intersect by adding properties:

type A = { foo: number } & { bar: string } // { foo: number, bar: string }

In contrast, primitive types intersect by reducing the possible set of values:

type B = string & 'abc' // 'abc'

And you can augment primitive types with new properties by intersecting them with an object type:

type C = string & { foo: number }
declare const c: C
c.foo // number

But a primitive can never be a different primitive. So intersecting two different primitive types will result in never

type D = string & number // never
type E = 1 & 2 // never

Lastly, void is a primitive type.


So, this all means that void & { foo: number } means that the primitive void will also have the property foo on it.

However, void & string will produce never because they are two different primitive types.

But, void & String is void with the properties of String because String is an object type (created by new String()).


This is all rather pointless, though. You can't assign anything besides undefined to void, and undefined can't have properties. So I'd argue that void & Type has no reason to exist in your codebase. And if you think you need that, I'd ask yourself why and try to refactor things to not need that.

Alex Wayne
  • 178,991
  • 47
  • 309
  • 337
  • 1
    Part of this is that `void` is *weird* and all of OPs manipulations made me say "oh, no, don't mess with `void`" when I first saw them. The `void` type is sort of like `unknown` (when it's a function return type) and sort of like `undefined` (when you start assigning things, or as function parameters). If it's `unknown`-like then it makes sense that intersecting with `{}` should probably produce `void` (or `void & {}`), but if it's `undefined`-like then intersecting with `{}` should reduce to `never` just like `undefined & {}` does. – jcalz Apr 10 '23 at 14:22
  • 1
    So the fact that it stays unreduced is probably the safest thing for them to have done, but the reasoning is going to involve facing the fact that `void` is weird and inconsistent. – jcalz Apr 10 '23 at 14:23