diff --git a/test/filter.test.ts b/test/filter.test.ts new file mode 100644 index 0000000..b4c5279 --- /dev/null +++ b/test/filter.test.ts @@ -0,0 +1,92 @@ +import { expectError, expectType } from 'tsd'; + +import { isNil, isNotNil, filter, gt, pipe, prop } from '../es'; + +type Foobar = { foo?: string; }; +type Values = { value: number }; +const gt5 = gt(5); + +// filter(() => val is p)(list) +// curried filter(isNotNil) with no type annotation defaults to `{}`, the the full function's return type ends up as {}[] +// the return type here is {}[] due to the "collapsing generic issue", read more about that here: https://github.com/ramda/types/discussions/54 +expectType<{}[]>(filter(isNotNil)([] as (string | undefined)[])); +// you can fix by setting the generic on isNotNil, the type gets narrows to be just `string` (intended purpose) +expectType(filter(isNotNil)([] as (string | undefined)[])); +// deeper types can't be narrowed by default with isNotNil +expectType(filter((x: Foobar) => isNotNil(x.foo))([] as Foobar[])); +// type annotations required for function composition with other ramda funcs +expectType( + filter( + pipe(prop('foo'), isNotNil) + // ^ this give typescript a "hint" that `prop('foo')(foobar: Foobar)`, which trickles out to both `pipe` an `isNotNil` here + )([] as Foobar[]) +); +// to narrow deeply, set custom return type on arrow function +expectType[]>(filter((x: Foobar): x is Required => isNotNil(x.foo))([] as Foobar[])); +// can do isNil too +expectType(filter(isNil)([] as (string | undefined)[])); +// combining with `pipe` requires same annotations +expectType(pipe(filter(isNotNil))([] as (string | undefined)[])); +// when using a function like gt5 which doesn't have a generic, passing filter to pipe has no negative consequences +expectType(pipe(filter(gt5))([] as number[])); + +// for when predicate doesn't type narrow + + +// filter(() => boolean)(list) +expectType(filter(gt5)([] as number[])); +// can't use and untyped arrow function +// @ts-expect-error +filter(x => gt5(x.value))([] as Values[]); // prop `value` does not exist on `unknown`, remove ts-expect-error to display error (test fails without) +// fix by setting the generic on `filter` +expectType(filter(x => gt5(x.value))([] as Values[])); +// or typing the arrow func +expectType(filter((x: Values) => gt5(x.value))([] as Values[])); +// using an inline `isNotNil` function that does not type narrow returns `number | undefined`, and not `number` like the tests above +expectType<(number | undefined)[]>(filter((x: number | undefined) => x != null)([] as (number | undefined)[])); +// combining with `pipe` requires same annotations +expectType(pipe(filter(x => gt5(x.value)))([] as Values[])); +expectType(pipe(filter((x: Values) => gt5(x.value)))([] as Values[])); +// when using a function like gt5 which doesn't have a generic, passing filter to pipe has no negative consequences +expectType(pipe(filter(gt5))([] as number[])); + + +// filter(() => narrow)(dist) +type Dict = Record; + +// same is as the first test, returns `Record` +expectType>(filter(isNotNil)({} as Dict)); +// can fix with a type annotation +expectType>(filter(isNotNil)({} as Dict)); + +// notice that `dict` is not inherently supported by `filter` when used with `pipe` +// this is because when a function is passed a function, it takes last overload, which only supports `list` +// this is a limitation of typescript, not ramda +expectError(pipe(filter(isNotNil))({} as Dict)); +// you can get around this by using an arrow function +expectType>(pipe((dict: Dict) => filter(isNotNil, dict))({} as Dict)); + +// filter(() => val is p, list) +// full signature can directly match list/dict type, and apply it to the generic on isNotNil +expectType(filter(isNotNil, [] as (string | undefined)[])); +// this also means it can infer `x` for arrow functions +expectType(filter(x => isNotNil(x.foo), [] as Foobar[])); +// as well as for function composition with other ramda funcs +expectType( + filter(pipe(prop('foo'), isNotNil), [] as Foobar[]) +); +// still need to set custom return type to narrow deeply, but you don't need to set the type on `x` +expectType[]>(filter((x): x is Required => isNotNil(x.foo), [] as Foobar[])); +// is Nil works un-annotated as well +expectType(filter(isNil, [] as (string | undefined)[])); + +// filter(() => boolean, list) +expectType(filter(gt5, [] as number[])); +// arrow function `x` arg get's type inferred correctly +filter(x => gt5(x.value), [] as Values[]); +// using an inline `isNotNil` function that does not type narrow returns `number | undefined` +expectType<(number | undefined)[]>(filter(x => x != null, [] as (number | undefined)[])); + +// filter(() => narrow, dist) +// no need for type annotations when using full signature +expectType>(filter(isNotNil, {} as Dict)); diff --git a/test/reject.test.ts b/test/reject.test.ts new file mode 100644 index 0000000..544f161 --- /dev/null +++ b/test/reject.test.ts @@ -0,0 +1,94 @@ +import { expectError, expectType } from 'tsd'; + +import { isNil, isNotNil, reject, gt, pipe, prop } from '../es'; + +type Foobar = { foo?: string; }; +type Values = { value: number }; +const gt5 = gt(5); + +// reject(() => val is p)(list) +// unlike `filter(isNotNil)`, `reject(isNil)` this works because `isNil`, and mostly by accident +// `reject(isNil)` return `(null | undefined) & T`, and `T` collapses to `{}`, +// but this combined with `Exclude` does yield the desired result of `string[]` +expectType(reject(isNil)([] as (string | undefined)[])); +// deeper types can't be narrowed by default with isNil +expectType(reject((x: Foobar) => isNil(x.foo))([] as Foobar[])); +// type annotations required for function composition with other ramda funcs +expectType( + reject( + pipe(prop('foo'), isNil) + // ^ this give typescript a "hint" that `prop('foo')(foobar: Foobar)`, which trickles out to both `pipe` an `isNil` here + )([] as Foobar[]) +); + +// You'd expect this to work same as `filter(pred)s`, but `Exclude>` is tyring to remove `foo: string` from `foo?: string` +// `foo` doesn't exist, only `foo?`, so `Exclude>` just ends up being `Foobar`. Typescript is weird sometimes +expectType(reject((x: Foobar): x is Required => !isNil(x.foo))([] as Foobar[])); +// can do isNotNil too +expectType(reject(isNotNil)([] as (string | undefined)[])); +// combining with `pipe` requires same annotations +expectType(pipe(reject(isNil))([] as (string | undefined)[])); +// when using a function like gt5 which doesn't have a generic, passing reject to pipe has no negative consequences +expectType(pipe(reject(gt5))([] as number[])); + +// for when predicate doesn't type narrow + + +// reject(() => boolean)(list) +expectType(reject(gt5)([] as number[])); +// can't use and untyped arrow function +// @ts-expect-error +reject(x => gt5(x.value))([] as Values[]); // prop `value` does not exist on `unknown`, remove ts-expect-error to display error (test fails without) +// fix by setting the generic on `reject` +expectType(reject(x => gt5(x.value))([] as Values[])); +// or typing the arrow func +expectType(reject((x: Values) => gt5(x.value))([] as Values[])); +// using an inline `isNil` function that does not type narrow returns `number | undefined`, and not `number` like the tests above +expectType<(number | undefined)[]>(reject((x: number | undefined) => x != null)([] as (number | undefined)[])); +// combining with `pipe` requires same annotations +expectType(pipe(reject(x => gt5(x.value)))([] as Values[])); +expectType(pipe(reject((x: Values) => gt5(x.value)))([] as Values[])); +// when using a function like gt5 which doesn't have a generic, passing reject to pipe has no negative consequences +expectType(pipe(reject(gt5))([] as number[])); + + +// reject(() => narrow)(dist) +type Dict = Record; + +// works for the same reason as the first test +expectType>(reject(isNil)({} as Dict)); +// can fix with a type annotation +expectType>(reject(isNil)({} as Dict)); + +// notice that `dict` is not inherently supported by `reject` when used with `pipe` +// this is because when a function is passed a function, it takes last overload, which only supports `list` +// this is a limitation of typescript, not ramda +expectError(pipe(reject(isNil))({} as Dict)); +// you can get around this by using an arrow function +expectType>(pipe((dict: Dict) => reject(isNil, dict))({} as Dict)); + +// reject(() => val is p, list) +// full signature can directly match list/dict type, and apply it to the generic on isNil +expectType(reject(isNil, [] as (string | undefined)[])); +// this also means it can infer `x` for arrow functions +expectType(reject(x => isNil(x.foo), [] as Foobar[])); +// as well as for function composition with other ramda funcs +expectType( + reject(pipe(prop('foo'), isNil), [] as Foobar[]) +); +// still need to set custom return type to narrow deeply, but you don't need to set the type on `x` +// see about why this returns `Foobar[]` while `filter()` returns `Required[]` +expectType(reject((x): x is Required => !isNil(x.foo), [] as Foobar[])); +// isNotNil works un-annotated as well +expectType(reject(isNotNil, [] as (string | undefined)[])); + +// reject(() => boolean, list) +expectType(reject(gt5, [] as number[])); +// arrow function `x` arg get's type inferred correctly +reject(x => gt5(x.value), [] as Values[]); +// using an inline `isNil` function that does not type narrow returns `number | undefined` +expectType<(number | undefined)[]>(reject(x => x != null, [] as (number | undefined)[])); + +// reject(() => narrow, dist) +// no need for type annotations when using full signature +expectType>(reject(isNil, {} as Dict)); diff --git a/types/filter.d.ts b/types/filter.d.ts index 8007257..8e47ed2 100644 --- a/types/filter.d.ts +++ b/types/filter.d.ts @@ -1,12 +1,24 @@ +// filter(() => narrow) export function filter( pred: (val: A) => val is P, ): { + // if we put `Record` first, it will actually pic up `A[]` as well + // so it needs to go first (list: readonly B[]): P[]; (dict: Record): Record; + // but we also want `A[]` to be the default when doing `pipe(filter(fn))`, so it also needs to be last + (list: readonly B[]): P[]; }; + +// filter(() => boolean) export function filter( pred: (value: T) => boolean, ):

>(collection: C) => C; + +// filter(() => narrow, list) - readonly T[] falls into Record for some reason, so list needs to come first export function filter(pred: (val: T) => val is P, list: readonly T[]): P[]; +// filter(() => narrow, dist) export function filter(pred: (val: T) => val is P, dict: Record): Record; +// filter(() => boolean, list | dist) - this case is not expected to be picked up directly +// it is here so operations like `flip(filter)` or `addIndex(filter)` get retained correctly type-wise (or best they can) export function filter>(pred: (value: T) => boolean, collection: C): C; diff --git a/types/reject.d.ts b/types/reject.d.ts index 4048101..4989a1f 100644 --- a/types/reject.d.ts +++ b/types/reject.d.ts @@ -1,18 +1,24 @@ +// reject(() => narrow) export function reject( pred: (val: A) => val is P, ): { - (list: readonly B[]): Array>; + // if we put `Record` first, it will actually pic up `A[]` as well + // so it needs to go first + (list: readonly B[]): Exclude[]; (dict: Record): Record>; + // but we also want `A[]` to be the default when doing `pipe(reject(fn))`, so it also needs to be last + (list: readonly B[]): Exclude[]; }; + +// reject(() => boolean) export function reject( pred: (value: T) => boolean, ):

>(collection: C) => C; -export function reject( - pred: (val: A) => val is P, - list: readonly B[], -): Array>; -export function reject( - pred: (val: A) => val is P, - dict: Record, -): Record>; + +// reject(() => narrow, list) - readonly T[] falls into Record for some reason, so list needs to come first +export function reject(pred: (val: T) => val is P, list: readonly T[]): Exclude[]; +// reject(() => narrow, dist) +export function reject(pred: (val: T) => val is P, dict: Record): Record>; +// reject(() => boolean, list | dist) - this case is not expected to be picked up directly +// it is here so operations like `flip(reject)` or `addIndex(reject)` get retained correctly type-wise (or best they can) export function reject>(pred: (value: T) => boolean, collection: C): C;