3

I have this simple function that checks if an unknown value looks like a Date object:

function looksLikeDate(obj: unknown): obj is { year: unknown; month: unknown; day: unknown } {
    return (
        obj !== null && typeof obj === "object" && "year" in obj && "month" in obj && "day" in obj
    );
}

But I get the following error for the "year" in obj part of the code:

Object is possibly 'null'. (2531)

When I switch obj !== null and typeof obj === "object" the error goes away: TS Playground Link

Isn't this strange? Can anybody explain this to me?

Krisztián Balla
  • 19,223
  • 13
  • 68
  • 84
  • Does this answer your question? [Typescript property type guards on unknown](https://stackoverflow.com/questions/69705488/typescript-property-type-guards-on-unknown) – captain-yossarian from Ukraine Oct 26 '21 at 09:18
  • 2
    @captain-yossarian: That doesn't explain that `typeof null` returns `'object'`, which is the problem here. – Cerbrus Oct 26 '21 at 09:20
  • Is there a reason your type-guard doesn't ensure that `year`, `month`, and `day` are typed as `number`? If they can be `string`-or-`number` then you should use `day: string | number`. – Dai Oct 26 '21 at 09:23
  • @Dai yes, they could be strings. – Krisztián Balla Oct 26 '21 at 09:24
  • 2
    This is because `obj !== null` does not narrow the `obj` to the `object` type. After this guard, `obj` is still `unknown`. TS does not support negation types, hence it is impossible to express `unknown | not null`. Whereas, `typeof obj === object` narrows the `obj` to specific union `object | null`. After that you are allowed to extract `null` with another one typeguard – captain-yossarian from Ukraine Oct 26 '21 at 09:28
  • @captain-yossarian TypeScript _does_ support "negation types" insofar as you can express `object | not(null)` with the `never` type. I think you an also express something equivalent to "`unknown && !null`" though with a different syntax. – Dai Oct 26 '21 at 09:32
  • @Dai I know about this workaround. How would you express type: `any number except 3` ?. It is possible to use `never` if you are able to infer this type like here https://catchts.com/type-negation but if you need to express this type literally, without function helper - it would be impossible to write smth like this: `type N = number | not 3` – captain-yossarian from Ukraine Oct 26 '21 at 09:38
  • @captain-yossarian I believe conditional-types should work for that, e.g.: `type Is3 = 3; type N = V extends Is3 ? never : V;`. – Dai Oct 26 '21 at 09:43
  • @Dai you are right, but you should still infer the value. In this case `3`. Maybe I was not clear enough. You can negate with help of extra utility, in other words if you are allowed to infer the type of value. – captain-yossarian from Ukraine Oct 26 '21 at 09:49

1 Answers1

2
typeof null === 'object'

If you do the typeof check after the null check, the obj can be null, even though you checked for null before, but the TS compiler is a little naïve.

Those type guards are a little fragile in that sense, as in your example, typeof obj === 'object' changed the type from "not null" to object | null

Cerbrus
  • 70,800
  • 18
  • 132
  • 147
  • Yea, it was, @KrisztiánBalla :D – Cerbrus Oct 26 '21 at 09:24
  • On that first line? That's intentional. The type of `null` is `'object'`. – Cerbrus Oct 26 '21 at 09:26
  • Thanks for the explanation. This is not how this should work imho. (A && B) and (B && A) should have the same result. – Krisztián Balla Oct 26 '21 at 09:27
  • 1
    Each subsequent type guard changes the type of the value, after it. Those type guards don't take earlier type guards into consideration. `typeof === 'object'` will always result in that something being typed `object | null`, which is why you need the `null` check after it. – Cerbrus Oct 26 '21 at 09:29