Improve `Includes` type
- Fully supports optional elements (?) → returns
booleanif the match is optional. - Full Supports for Arrays (Rest element)
- Correctly handles required undefined separately from optional (?).
| Feature | Old | New |
|---|---|---|
| Optional element support | ❌ | ✅ |
| Required undefined distinction | ❌ | ✅ |
| Rest element support | ❌ | ✅ |
| Result precision | true/false |
true/false/boolean |
| Edge case coverage | limited | complete |
After consideration a recursive types is better to cover the rest element cases. if there is any trick that can be used in the first maped object type to handle the rest element, whoud that be better ?
Suggestions for some more tests:
expectType<Includes<readonly [1, 2?, 3?], 2>>(boolean);
expectType<Includes<[1?, 2?, 3?], 1>>(boolean);
expectType<Includes<[1?, 2?, 3?], 4>>(false);
expectType<Includes<[unknown], unknown>>(true);
expectType<Includes<[unknown], string>>(false);
expectType<Includes<[1, unknown], unknown>>(true);
expectType<Includes<[null], null>>(true);
expectType<Includes<[null], undefined>>(false);
expectType<Includes<[true, false], true>>(true);
expectType<Includes<[true, false], boolean>>(false);
expectType<Includes<[boolean], boolean>>(true);
@sindresorhus
expectType<Includes<[true, false], boolean>>(false);
Umm, this should be true, because:
Includes<[true, false], boolean>
=> Includes<[true, false], true> | Includes<[true, false], false>
=> true | true
=> true
expectType<Includes<[boolean], boolean>>(true);
And, this should be boolean, because:
Includes<[boolean], boolean>
=> Includes<[true], true> | Includes<[true], false> | Includes<[false], true> | Includes<[false], false>
=> true | false | false | true
=> boolean
@som-sm Includes here is exact-equality (IsEqual) based. No assignability, no distribution over Item. So I feel like this is correct:
-
Includes<[true, false], boolean>⇒ false* -
Includes<[boolean], boolean>⇒ true - It only returns
booleanwhen uncertainty is introduced by optionals/rest.
@som-sm
Includeshere is exact-equality (IsEqual) based. No assignability, no distribution overItem. So I feel like this is correct:
@sindresorhus Umm...ok gotcha, but in my experience distributing is usually a good idea. I'm not sure of good use cases for this type, but here's a quick, contrived example:
declare function includes<
const T extends readonly unknown[],
const Target extends Includes<T, Target> extends false ? never : unknown,
>(array: T, target: Target): void;
includes([1, 2, 3] as [1, 2, 3 | 4], 4); // Errors, but shouldn't
// Argument of type '4' is not assignable to parameter of type 'never'
@benzaria Could you share some practical, real-world examples for this type?
I intentionally designed Includes to be strict following the original behavior.
But @som-sm had a good point of adding distribution, which can be useful.
I can add an option, that triggers it and make it false by default.
@sindresorhus WDYT
Yeah, I think a {distributeItem} option could be useful, but it should be off by default.
Missed this, it should be distributeItem singular, not plural. We are distributing the "Item" type.
Missed this, it should be
distributeItemsingular, not plural. We are distributing the "Item" type.
I did notice that, but in this case, we’re actually distributing both the Item type and the array element types — that’s why I chose the plural form. Would you still prefer keeping it singular for consistency?
I still prefer distributeItem (singular). While array elements do get distributed, that's an implementation detail of the comparison logic. The option specifically controls whether to distribute the Item parameter. The name should describe what the user is controlling (distribution of the Item type), not all the internal mechanics.