remeda
Version:
A utility library for JavaScript and Typescript.
1 lines • 6.89 kB
Source Map (JSON)
{"version":3,"file":"isIncludedIn.cjs","names":[],"sources":["../src/isIncludedIn.ts"],"sourcesContent":["import type { IsLiteral, IsUnion } from \"type-fest\";\nimport type { IterableContainer } from \"./internal/types/IterableContainer\";\n\n/**\n * A \"constant\" tuple is a type that has a single runtime value that can fulfil\n * it. This means that it doesn't have any variadic/rest/spread/array parts, and\n * that all it's values are singular (non-union) literals.\n *\n * We use this type to allow narrowing when checking against a set of values\n * defined as a const.\n *\n * @example\n * type T1 = IsConstantTuple<[\"cat\", \"dog\", 3, true]>; // => true;\n * type T2 = IsConstantTuple<[\"cat\" | \"dog\"]>; // false;\n * type T2 = IsConstantTuple<[\"cat\", ...Array<\"cat\">]>; // false;\n */\ntype IsConstantTuple<T extends IterableContainer> = T extends readonly []\n ? true\n : T extends readonly [infer Head, ...infer Rest]\n ? IsUnion<Head> extends true\n ? false\n : IsConstantTuple<Rest>\n : false;\n\n/**\n * There is no way to tell Typescript to only narrow the \"accepted\" side of a\n * type-predicate and so in many cases the negated side is also affected, this\n * results in over-narrowing in many cases, breaking typing. For this reason we\n * only want to use the type-predicate variant of `isIncludedIn` when we can\n * assume the result represents the expected types (closely enough). This is not\n * and ideal solution and we will still generate wrong types in some cases (see\n * tests), but it reduces the surface of this problem significantly, while still\n * keeping the utility of `isIncludedIn` for the common cases.\n *\n * TL;DR - The types are narrowable when: T is literal and S is a pure tuple, or\n * when T isn't a literal, but S is.\n *\n * @example\n * const data = 1 as 1 | 2 | 3;\n * const container = [] as Array<1 | 2>;\n * if (isIncludedIn(data, container)) {\n * ... it makes sense to narrow data to `1 | 2` as the value `3` is not part\n * ... of the typing of container, so will never result in being true.\n * } else {\n * ... but it doesn't make sense to narrow the value to 3 here, because 1\n * ... and 2 are still valid values for data, when container doesn't include\n * ... them **at runtime**.\n * ... Typescript narrows the _rejected_ branch based on how it narrowed the\n * ... _accepted_ clause, and we can't control that; because our input type\n * ... is `1 | 2 | 3` and the accepted side is `1 | 2`, the rejected side is\n * ... typed `Exclude<1 | 2 | 3, 1 | 2>`, which is `3`.\n * }\n * }\n */\ntype IsNarrowable<T, S extends IterableContainer<T>> =\n IsLiteral<T> extends true\n ? // When T is literal (i.g. it isn't a primitive type like `string` or\n // `number`) then the criteria for narrowing is that the container is a\n // \"pure\" tuple because we *assume* that S represents a constant set of\n // values, and that it's typing also represents it's runtime content 1-\n // for-1. If S isn't a pure tuple it means we can't tell from the typing\n // which of it's values are actually present in runtime so can't use them\n // to narrow correctly.\n IsConstantTuple<S>\n : // When T isn't a literal type but the items in S are we can narrow the\n // type because it won't affect the negated side (`Exclude<number, 3>`\n // is still `number`).\n IsLiteral<S[number]>;\n\n/**\n * Checks if the item is included in the container. This is a wrapper around\n * `Array.prototype.includes` and `Set.prototype.has` and thus relies on the\n * same equality checks that those functions do (which is reference equality,\n * e.g. `===`). In some cases the input's type is also narrowed to the\n * container's item types.\n *\n * Notice that unlike most functions, this function takes a generic item as it's\n * data and **an array** as it's parameter.\n *\n * @param data - The item that is checked.\n * @param container - The items that are checked against.\n * @returns `true` if the item is in the container, or `false` otherwise. In\n * cases the type of `data` is also narrowed down.\n * @signature\n * R.isIncludedIn(data, container);\n * @example\n * R.isIncludedIn(2, [1, 2, 3]); // => true\n * R.isIncludedIn(4, [1, 2, 3]); // => false\n *\n * const data = \"cat\" as \"cat\" | \"dog\" | \"mouse\";\n * R.isIncludedIn(data, [\"cat\", \"dog\"] as const); // true (typed \"cat\" | \"dog\");\n * @dataFirst\n * @category Guard\n */\nexport function isIncludedIn<T, S extends IterableContainer<T>>(\n data: T,\n container: IsNarrowable<T, S> extends true ? S : never,\n): data is S[number];\nexport function isIncludedIn<T, S extends T>(\n data: T,\n container: IterableContainer<S>,\n): boolean;\n\n/**\n * Checks if the item is included in the container. This is a wrapper around\n * `Array.prototype.includes` and `Set.prototype.has` and thus relies on the\n * same equality checks that those functions do (which is reference equality,\n * e.g. `===`). In some cases the input's type is also narrowed to the\n * container's item types.\n *\n * Notice that unlike most functions, this function takes a generic item as it's\n * data and **an array** as it's parameter.\n *\n * @param container - The items that are checked against.\n * @returns `true` if the item is in the container, or `false` otherwise. In\n * cases the type of `data` is also narrowed down.\n * @signature\n * R.isIncludedIn(container)(data);\n * @example\n * R.pipe(2, R.isIncludedIn([1, 2, 3])); // => true\n * R.pipe(4, R.isIncludedIn([1, 2, 3])); // => false\n *\n * const data = \"cat\" as \"cat\" | \"dog\" | \"mouse\";\n * R.pipe(\n * data,\n * R.isIncludedIn([\"cat\", \"dog\"] as const),\n * ); // => true (typed \"cat\" | \"dog\");\n * @dataLast\n * @category Guard\n */\nexport function isIncludedIn<T, S extends IterableContainer<T>>(\n container: IsNarrowable<T, S> extends true ? S : never,\n): (data: T) => data is S[number];\nexport function isIncludedIn<T, S extends T>(\n container: IterableContainer<S>,\n): (data: T) => boolean;\n\nexport function isIncludedIn(\n dataOrContainer: unknown,\n container?: ReadonlyArray<unknown>,\n): unknown {\n if (container === undefined) {\n // === dataLast ===\n // We don't use purry here because we can optimize the dataLast case by\n // memoizing a set and accessing it in O(1) time instead of scanning the\n // array **each time** (O(n)) each time.\n const asSet = new Set(dataOrContainer as ReadonlyArray<unknown>);\n return (data: unknown) => asSet.has(data);\n }\n\n // === dataFirst ===\n return container.includes(dataOrContainer);\n}\n"],"mappings":"AAyIA,SAAgB,EACd,EACA,EACS,CACT,GAAI,IAAc,IAAA,GAAW,CAK3B,IAAM,EAAQ,IAAI,IAAI,EAA0C,CAChE,MAAQ,IAAkB,EAAM,IAAI,EAAK,CAI3C,OAAO,EAAU,SAAS,EAAgB"}