Is it possible in TypeScript to implement a filter function that removes/excludes basic types from an existing type (with proper type re-casting), using the following signature? -
type basicType = 'string' | 'number' | 'boolean' | 'bigint';
function removeByType<T>(input: T[], ...types: basicType[]): Exclude<T, ?>[] {
// actual data filtering (irrelevant for this question)
}
I'm not worried about the exact signature, as long as it can be used like this (or similar):
const input = [1, 2, 3, 'four', false]; //=> Array<number | string | boolean>
const res = removeByType(input, 'string', 'boolean'); //=> Array<number>
And if it is not possible, then what would be the next best thing for this?
So far I have only been able to make it work for just a single-value parameter, through these re-declarations:
function removeByType<T>(input: T[], t: 'string'): Exclude<T, string>[];
function removeByType<T>(input: T[], t: 'number'): Exclude<T, number>[];
function removeByType<T>(input: T[], t: 'boolean'): Exclude<T, boolean>[];
function removeByType<T>(input: T[], t: 'bigint'): Exclude<T, bigint>[];
I'm trying to figure out how to make it work for a list, or if it is at all possible...
UPDATE
The final solution ended up here, as it was intended for a custom operator of iter-ops library. Many thanks to Jared Smith for his answer!
CodePudding user response:
The problem with this question is that types have no term-level representation (the values given from the runtime typeof
operator are just strings for instance), and you can't go from types -> terms the way you can with e.g. Haskell typeclass metaprogramming. Implementing this at the type level isn't hard if we tweak the signature a bit, implementing this at the term level involves...? So the implementation logic isn't necessarily irrelevant.
I've implemented it below using a map to map the string literal types for the primitives with the actual type and with liberal casting. Maybe somebody else will come up with a cleverer solution here that preserves more type safety.
type PrimitiveMap = {
string: string;
number: number;
boolean: boolean;
undefined: undefined;
symbol: symbol;
// etc
}
type PrimitivesAsStrings = keyof PrimitiveMap;
type Primitives = PrimitiveMap[PrimitivesAsStrings]
function removeByType<
T extends Primitives,
R extends PrimitivesAsStrings
>(input: T[], ...exclusions: R[]): Exclude<T, PrimitiveMap[R]>[] {
return input.filter((item) => exclusions.includes(typeof item as R)) as any;
}
const test = [1, 2, true, 'hi'];
const onlyNum = removeByType(test, 'boolean', 'string'); // number[]
One final note of caution: I usually call code like this "clever" and that isn't necessarily a compliment. Carefully weigh the pros and cons of including something like this in the code base, and whether you're getting enough safety to weigh against the complexity. Type system trickery is always fun but only occasionally worth it for production code IMO.
CodePudding user response:
Below is the half-answer I created on my own while playing with it:
type basicType<T> = T extends 'string' ? string :
T extends 'number' ? number :
T extends 'boolean' ? boolean :
T extends 'bigint' ? bigint :
never;
function removeByType<T, A>(input: T[], t1: keyof A): Exclude<T, basicType<typeof t1>>[];
function removeByType<T, A, B>(input: T[], t1: keyof A, t2: keyof B): Exclude<T, basicType<typeof t1 | typeof t2>>[];
function removeByType<T, A, B, C>(input: T[], t1: keyof A, t2: keyof B, t3: keyof C): Exclude<T, basicType<typeof t1 | typeof t2 | typeof t3>>[];
function removeByType<T, A, B, C, D>(input: T[], t1: keyof A, t2: keyof B, t3: keyof C, t4: keyof D): Exclude<T, basicType<typeof t1 | typeof t2 | typeof t3 | typeof t4>>[];
function removeByType<T, A>(input: T[], ...t: (keyof A)[]): Exclude<T, any> {
return input.filter(a => {
return t.indexOf(typeof a as any) < 0;
}) as any;
}
It works fine, but the problem with it - values are not enforced, i.e. you can pass in anything like bla-bla
string into it, and TS won't complain.
That's why the earlier solution from Jared Smith is better. Unless, this one can be easily fixed, somehow?