sindresorhus / type-fest

A collection of essential TypeScript types
Creative Commons Zero v1.0 Universal
13.64k stars 515 forks source link

`RequireAtLeastOne`: Type does not narrow as expected #230

Open joealden opened 3 years ago

joealden commented 3 years ago

With the following example:

import { RequireAtLeastOne } from "type-fest";

type T = RequireAtLeastOne<{ a: string; b: string }>;

const getResult = (value: T) => value.a ?? value.b;

getResult has a return type of string | undefined, but I would expect it to have a return type of string, as if value.a is undefined, value.b should have a type of string? Would this be possible to achieve?

Note that I have tested this with the latest version of typescript at the time of writing (4.3.5).

Upvote & Fund

Fund with Polar

sindresorhus commented 3 years ago

You are not passing in the required keys. See the example in https://github.com/sindresorhus/type-fest/blob/main/source/require-at-least-one.d.ts

joealden commented 3 years ago

@sindresorhus from my understanding, omitting the 2nd type argument in RequireAtLeastOne is the same as specifying all the keys of the object passed in as the 1st type argument, so the following two types are equivalent:

type T1 = RequireAtLeastOne<{ a: string; b: string }>;
type T2 = RequireAtLeastOne<{ a: string; b: string }, "a" | "b">;

Let me know if I'm misunderstanding the usage intent for RequireAtLeastOne, but in the example, I would expect that TS would be able to deduce (via type narrowing) that in the expression value.a ?? value.b, if value.b is going to be returned, because either a or b is required (one must exist), b must exist (as a didn't, as if it did, then the right hand side of the expression wouldn't be reached), which would mean that getResult would have a return type of just string?

sindresorhus commented 3 years ago

I think https://github.com/sindresorhus/type-fest/commit/6110607eb99d25c7670df59d160296fcda1ac123 changed the behavior of this.

// @ulken

aoberoi commented 2 years ago

I ran into the same issue, even when I pass the keys as the second type parameter. I believe the RequestAtLeastOne type's implementation just doesn't play nice with type narrowing. I'm not sure if there's an alternative implementation that would play nice in this way. For now, it seems like writing a user-defined type guard would be the best way to narrow. A less safe shortcut would be to use a non-null assertion (value.b!).

nmussy commented 4 months ago

Same thing happens with RequireExactlyOne:

(params: RequireExactlyOne<{ a: string; b: string }>): string => {
    if (params.a) return params.a;
    return params.b;
    ~~~~~~
    // Type 'string | undefined' is not assignable to type 'string'.
    //  Type 'undefined' is not assignable to type 'string'.ts(2322)
};
elijaholmos commented 3 weeks ago

I am experiencing this same issue as well. Type does not narrow as expected in VSCode, and all keys passed as the second type parameter are treated as optional, even if I perform optional checks like shown in the examples above.