Skip to content

Add Filter type #1183

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
68 changes: 68 additions & 0 deletions source/array-filter.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import type {OptionalKeysOf} from './optional-keys-of.d.ts';
import type {IsTruthy, Extends} from './internal/type.d.ts';
import type {UnknownArray} from './unknown-array.d.ts';
import type {CleanEmpty} from './internal/array.d.ts';
import type {IsAny} from './is-any.d.ts';

/**
Returns a boolean for whether a value `T` extends the filtering type `U`.

If `U` is `Boolean`, it checks whether `T` is `truthy` like {@link Boolean `Boolean(T)`} does.

Otherwise, it uses {@link Extends `Extends<T, U, S>`} to check if `T extends U` with strict or loose mode.
*/
type FilterType<T, U, S extends boolean> =
Boolean extends U
? IsTruthy<T>
: Extends<T, U, S>;

/**
Determines whether the array `V` should be kept based on the boolean type `T`.
*/
type IfFilter<T extends boolean, V extends UnknownArray> = [T] extends [true] ? V : [];

/**
Filters elements from an `Array_` based on whether they match the given `Type`.

If `Type` is `Boolean`, it filters out `falsy` values like {@link Boolean `Boolean(T)`} does.

Strict controls whether strict or loose type comparison is used (defaults to loose).
*/
export type ArrayFilter<
Array_ extends UnknownArray, Type,
Strict extends boolean = false,
> = IsAny<Array_> extends true ? []
: CleanEmpty<_ArrayFilter<Array_, Type, Strict>>;

/**
Internal implementation of {@link ArrayFilter}.

Iterates through the array and includes elements in the accumulator if they pass `FilterType`.
*/
type _ArrayFilter<
Array_ extends UnknownArray, Type,
Strict extends boolean = false,
Head extends any[] = [],
Tail extends any[] = [],
> =
keyof Array_ & `${number}` extends never // Is `Array_` leading a rest element or empty
? Array_ extends readonly [...infer Rest, infer Last]
? _ArrayFilter<Rest, Type, Strict, Head, [
...IfFilter<FilterType<Last, Type, Strict>, [Last]>,
...Tail,
]>
: [
...Head,
...IfFilter<FilterType<Array_[number], Type, Strict>, Array_>,
...Tail,
]
: Array_ extends readonly [(infer First)?, ...infer Rest]
? _ArrayFilter<Rest, Type, Strict, [
...Head,
...IfFilter<FilterType<First, Type, Strict>,
'0' extends OptionalKeysOf<Array_> // TODO: replace with `IsOptionalKeyOf`.
? [First?]
: [First]
>,
], Tail>
: never;
56 changes: 55 additions & 1 deletion source/internal/array.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import type {If} from '../if.d.ts';
import type {IsAny} from '../is-any.d.ts';
import type {IsNever} from '../is-never.d.ts';
import type {OptionalKeysOf} from '../optional-keys-of.d.ts';
import type {IsUnion} from '../is-union.d.ts';
import type {EmptyObject} from '../empty-object.d.ts';
import type {UnknownArray} from '../unknown-array.d.ts';
import type {OptionalKeysOf} from '../optional-keys-of.d.ts';
import type {IsExactOptionalPropertyTypesEnabled, IfNotAnyOrNever} from './type.d.ts';

/**
Expand Down Expand Up @@ -96,6 +99,35 @@ Returns whether the given array `T` is readonly.
*/
export type IsArrayReadonly<T extends UnknownArray> = If<IsNever<T>, false, T extends unknown[] ? false : true>;

/**
Represents an empty array, the `[]` or `readonly []` value.
*/
export type EmptyArray = readonly [] | []; // The extra `[]` is just to prevent TS from expanding the type.

/**
Returns a `boolean` for whether the type is an empty array, the `[]` or `readonly []` value.
@example
```
import type {IsEmptyArray} from 'type-fest';
type Pass1 = IsEmptyArray<[]>;
//=> true
type Pass2 = IsEmptyArray<readonly []>;
//=> true
type Fail1 = IsEmptyArray<[0]>;
//=> false
type Fail2 = IsEmptyArray<[0?]>;
//=> false
type Fail3 = IsEmptyArray<...string[]>;
//=> false
```
@see EmptyArray
@category Array
*/
export type IsEmptyArray<T> =
IsNever<T> extends true ? false
: T extends EmptyArray ? true
: false;

/**
Transforms a tuple type by replacing it's rest element with a single element that has the same type as the rest element, while keeping all the non-rest elements intact.

Expand Down Expand Up @@ -156,3 +188,25 @@ type _CollapseRestElement<
>
: never // Should never happen, since `[(infer First)?, ...infer Rest]` is a top-type for arrays.
: never; // Should never happen

/**
Cleans any extra empty arrays/objects from a union.

@example
```
type T1 = CleanEmpty<[number] | []>
//=> [number]

type T2 = CleanEmpty<[number, string?] | [never] | []>
//=> [number, string?] | [never]

type T3 = CleanEmpty<[]>
//=> []
```

@category Utilities
*/
export type CleanEmpty<T> =
Exclude<T, EmptyArray | EmptyObject> extends infer U
? IsNever<U> extends true ? T : U
: never;
34 changes: 33 additions & 1 deletion source/internal/type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {If} from '../if.d.ts';
import type {IsAny} from '../is-any.d.ts';
import type {IsNever} from '../is-never.d.ts';
import type {Primitive} from '../primitive.d.ts';
import type {ExtendsStrict} from '../extends-strict.d.ts';

/**
Matches any primitive, `void`, `Date`, or `RegExp` value.
Expand Down Expand Up @@ -100,9 +101,40 @@ type C = IfNotAnyOrNever<never, 'VALID', 'IS_ANY', 'IS_NEVER'>;
export type IfNotAnyOrNever<T, IfNotAnyOrNever, IfAny = any, IfNever = never> =
If<IsAny<T>, IfAny, If<IsNever<T>, IfNever, IfNotAnyOrNever>>;

/*
/**
Indicates the value of `exactOptionalPropertyTypes` compiler option.
*/
export type IsExactOptionalPropertyTypesEnabled = [(string | undefined)?] extends [string?]
? false
: true;

/**
Evaluates whether type `T extends U`, using either strict or loose comparison.

- Strict mode, {@link ExtendsStrict `ExtendsStrict<T, U>`} is used.
- Loose mode, {@link ExtendsLoose `ExtendsLoose<T, U>`} is used.
*/
export type Extends<T, U, S extends boolean = false> = {
true: ExtendsStrict<T, U>;
false: ExtendsLoose<T, U>;
}[`${S}`];

/**
Performs a loose type comparison: checks if wheither of the members in `T` extends `U`.

This is useful when needing to know if `T extends U` without distributing `T` in Main type
*/
export type ExtendsLoose<T, U> = IsNotFalse<T extends U ? true : false>;

/**
A union of `falsy` types in JS.
*/
export type Falsy = false | 0 | '' | null | undefined; // `| never`

/**
Checks if `T` is {@link Falsy `falsy`} similar to `Boolean(T)`.
*/
export type IsTruthy<T> =
IsNever<T> extends true ? false
: T extends Falsy ? false
: true; // ? Should this get exposed publicly
3 changes: 1 addition & 2 deletions source/is-union.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ export type IsUnion<T> = InternalIsUnion<T>;
/**
The actual implementation of `IsUnion`.
*/
type InternalIsUnion<T, U = T> =
(
type InternalIsUnion<T, U = T> = (
IsNever<T> extends true
? false
: T extends any
Expand Down
18 changes: 18 additions & 0 deletions source/object-filter.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type {UnknownRecord} from './unknown-record.d.ts';
import type {CleanEmpty} from './internal/array.d.ts';
import type {FilterType} from './array-filter.d.ts';
import type {Simplify} from './simplify.d.ts';

/**
Filters properties from an object where the property value matches the given type.

If `Type` is `Boolean`, it filters out `falsy` values like `Boolean(T)` does.

Strict controls whether strict or loose type comparison is used (defaults to loose).
*/
export type ObjectFilter<
Object_ extends UnknownRecord, Type,
Strict extends boolean = false,
> = CleanEmpty<Simplify<{
[K in keyof Object_ as FilterType<Object_[K], Type, Strict> extends true ? K : never]: Object_[K]
}>>;
115 changes: 115 additions & 0 deletions test-d/array-filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import {expectType} from 'tsd';
import type {ArrayFilter} from '../source/array-filter.d.ts';

// Basic loose filtering
expectType<ArrayFilter<[1, 2, 3, 3, 4], 3, true>>([3, 3]);
expectType<ArrayFilter<[1, '2', 3, 'foo', false], number>>([1, 3]);
expectType<ArrayFilter<[1, '2', 3, 'foo', false], string>>(['2', 'foo']);
expectType<ArrayFilter<[1, '2', 3, 'foo', false], string | number>>([1, '2', 3, 'foo']);
expectType<ArrayFilter<['foo', 'baz', 'foo', 'foo'], 'foo', true>>(['foo', 'foo', 'foo']);
expectType<ArrayFilter<[1, '2', 3, 'foo', false], string | number, true>>([1, '2', 3, 'foo']);
expectType<ArrayFilter<['1', '2', 3, 4, 'foo'], `${number}`>>(['1', '2']);
expectType<ArrayFilter<[true, false, true, 0, 1], boolean>>([true, false, true]);
expectType<ArrayFilter<[true, false, true, 0, 1], true>>([true, true]);

// Filtering Boolean (keep truthy values)
expectType<ArrayFilter<[true, false, boolean, 0, 1], Boolean>>([true, 1]);
expectType<ArrayFilter<[true, false, boolean, 0, 1], Boolean, true>>([true, 1]);
expectType<ArrayFilter<[0, '', false, null, undefined, 'ok', 42], Boolean>>(['ok', 42]);
expectType<ArrayFilter<[true, false, 0, 1, '', 'text', null, undefined], Boolean>>([true, 1, 'text']);

// Filtering objects
type Object1 = {a: number};
type Object2 = {b: string};
expectType<ArrayFilter<[Object1, Object2, Object1 & Object2], Object1>>({} as [Object1, Object1 & Object2]);
expectType<ArrayFilter<[Object1, Object2, Object1 & Object2], Object1, true>>({} as [Object1, Object1 & Object2]);

// Loose filtering by boolean or number
expectType<ArrayFilter<[true, 0, 1, false, 'no'], boolean | number>>([true, 0, 1, false]);

// Filtering array containing null | undefined | string
expectType<ArrayFilter<[null, undefined, 'foo', ''], string>>(['foo', '']);

// Filtering with unknown type (should keep everything)
expectType<ArrayFilter<[1, 'a', true], unknown>>([1, 'a', true]);

// Filtering with any type (should keep everything)
expectType<ArrayFilter<[1, 'a', true], any>>([1, 'a', true]);

// Filtering with never type (should remove everything)
expectType<ArrayFilter<[1, 2, 3], never>>([]);
// ? Shoud we change this behavior ?

// Filtering array of arrays by array type
expectType<ArrayFilter<[[number], string[], number[]], number[]>>([[1], [2, 3]]);

// Filtering by a union including literal and broader type
expectType<ArrayFilter<[1, 2, 3, 'foo', 'bar'], 1 | string>>([1, 'foo', 'bar']);

// Filtering complex nested union types
type Nested = {x: string} | {y: number} | null;
expectType<ArrayFilter<[ {x: 'a'}, {y: 2}, null, {z: true} ], Nested>>([{x: 'a'}, {y: 2}, null]);

// Filtering with boolean type but array has no boolean values
expectType<ArrayFilter<[1, 2, 3], Boolean>>([1, 2, 3]);

// Filtering with boolean type but array has falsy values
expectType<ArrayFilter<[0, '', false, null, undefined], Boolean>>([]);

// Filtering string literals with template literal union
expectType<ArrayFilter<['foo1', 'bar2', 'foo3'], `foo${number}`>>(['foo1', 'foo3']);

// Filtering with `Boolean` type but including custom objects with truthy/falsy behavior
class Foo {}
expectType<ArrayFilter<[typeof Foo, {}, null, undefined], Boolean>>([Foo, {}]);

// Filtering with strict = true and union including literals and primitives
expectType<ArrayFilter<[1, '1', 2, '2', true, false], number | `${number}`, true>>([1, '1', 2, '2']);

// Filtering falsy values mixed with ({} | [] is truthy)
expectType<ArrayFilter<[false, 0, '', null, undefined, {}, []], Boolean>>([{}, []]);

// Filtering with `true` literal (strict) but array contains boolean and number
expectType<ArrayFilter<[true, false, 1, 0], true, true>>([true]);

// Filtering empty string literal type with strict mode
expectType<ArrayFilter<['', 'non-empty'], '', true>>(['']);

// Filtering with loose mode for literal union type and matching subset
expectType<ArrayFilter<[1, 2, 3, 4, 5], 2 | 3>>([2, 3]);

// Filtering tuples with mixed optional and required elements
type Tuple = [string, number?, boolean?];
expectType<ArrayFilter<Tuple, number>>({} as [number?]);
expectType<ArrayFilter<Tuple, string | boolean>>({} as [string, boolean?]);

// Rest elements
expectType<ArrayFilter<['f', ...string[], 's'], string>>({} as ['f', ...string[], 's']);
expectType<ArrayFilter<['f', ...string[], 's'], 'f' | 's'>>({} as ['f', 's']);
expectType<ArrayFilter<[string, ...string[]], string>>({} as [string, ...string[]]);
expectType<ArrayFilter<[string, ...string[], number], string>>({} as [string, ...string[]]);

// Rest and Optional
expectType<ArrayFilter<[true, number?, ...string[]], string>>({} as string[]);
expectType<ArrayFilter<[true, number?, ...string[]], number | string>>({} as [number?, ...string[]]);
expectType<ArrayFilter<[string?, ...string[]], number | string>>({} as [string?, ...string[]]);

// Union
expectType<ArrayFilter<[1, '2', 3, false] | ['1', 2, '3', true], number>>({} as [1, 3] | [2]);
expectType<ArrayFilter<[1, '2', 3, false] | ['1', 2, '3', true], string>>({} as ['2'] | ['1', '3']);
expectType<ArrayFilter<[true, number?, ...string[]] | [false?, ...Array<'foo'>], string>>({} as string[] | Array<'foo'>);
expectType<ArrayFilter<[true, number?, ...string[]] | [false?, ...Array<'foo'>], number>>({} as [number?]);

// Edge cases
expectType<ArrayFilter<any, any>>({} as []);
expectType<ArrayFilter<any, never>>([]);
expectType<ArrayFilter<any[], any>>({} as []);
expectType<ArrayFilter<any[], never>>({} as any[]);
expectType<ArrayFilter<never, any>>({} as never);
expectType<ArrayFilter<never, never>>({} as never);

expectType<ArrayFilter<[], number>>([]);
expectType<ArrayFilter<[never, never], number>>([]);
expectType<ArrayFilter<[never, never], never>>([]);
expectType<ArrayFilter<[never, never], never>>([]);
expectType<ArrayFilter<[never, never], never, true>>({} as [never, never]);