Reputation: 9113
This question concerns the latest version of TypeScript, and so may be updated as newer versions come out.
Let's say I have the following set of types:
type A = {type: "A"; a_data : string};
type B = {type : "B"; b_data : string};
type C = {type : "C'; c_data : string};
And I define the following disjunction type:
type AnyABC = A | B | C;
My goal is to have a function, of the approximate form: (You can remove/add some parameters.)
function groupByKey(array : AnyABC[], key : (abc : AnyABC) => string)
That will accept an array of a mix of A, B, C
objects and return an object or an ES6 Map
with separate bins for A[]
, B[]
, and C[]
. So you could go:
let a = result.A[0]
And a
would be strongly typed as A
.
The actual code for doing this is just simple Javascript and less important. More important is how to encode the types correctly.
Even if there are no A
-type values in AnyABC[]
, I still want the A
bin to be an empty array []
. This means that the Javascript has to get some kind of object that contains a list of keys, so it can't all be eraseable type information.
I have my own attempt at the solution, which I'm going to add as an answer, but I'm looking for other, possibly better solutions too.
Upvotes: 0
Views: 59
Reputation: 328292
You seem to have thought this out, so I don't know if my answer is an improvement. The best I can do is this:
function groupByKeys<T>(
arr: T[keyof T][],
selector: (item: T[keyof T]) => keyof T,
possibleKeys: (keyof T)[],
): ArrayProps<T>;
function groupByKeys<T>(
arr: T[keyof T][],
selector: (item: T[keyof T]) => keyof T
): Partial<ArrayProps<T>>;
function groupByKeys<T>(
arr: T[keyof T][],
selector: (item: T[keyof T]) => keyof T,
possibleKeys?: (keyof T)[],
): Partial<ArrayProps<T>> {
const ret = {} as {[K in keyof T]: T[K][]};
if (possibleKeys) {
possibleKeys.forEach(k => ret[k] = []);
}
arr.forEach(i => {
const k = selector(i);
if (!(k in ret)) {
ret[k] = [];
}
ret[k].push(i);
})
return ret;
}
So my possibleKeys
parameter differs from yours in that it's an array, not an object. Presumably Object.keys(possibleKeys)
on yours results in mine.
type A = { type: 'A', a_data: string };
type B = { type: 'B', b_data: string };
type C = { type: 'C', c_data: string };
type AnyABC = A | B | C;
declare const arr: AnyABC[];
Here's how you call it:
type ABCHolder = { A: A, B: B, C: C };
groupByKeys<ABCHolder>(arr, i => i.type);
or, if you absolutely care about the output having some empty arrays instead of possibly missing some keys:
groupByKeys<ABCHolder>(arr, i => i.type, ['A','B','C']);
So, this works about the same as yours, with some differences. Upsides:
No meaningless inputs; instead you specify the type parameter and it gets erased. Or you pass in the type parameter and a list of keys (which is a bit redundant), but you're not passing in any nulls.
Since you specify that type parameter it won't let you put in 'D'
in possibleKeys
.
You are also prevented from doing x => "hello"
.
Downsides:
It would be nice to not need the type parameter or indeed refer to anything like the type {A:A, B:B, C:C}
, but such type inference is apparently a little much for the compiler. (A possible refactor is to pass in the actual output object as a parameter, and it will copy results into it. If you're willing to do that I might have a different implementation for you.)
Even though x => "hello"
is excluded, it still allows x => 'A'
, which is bad because x
might be of type B
or C
. Ideally the selector function is of the generic type <K extends keyof T>(item: T[K])=>K
, which expresses the exact constraint you want... but the compiler absolutely cannot witness that x => x.type
matches that, and you are forced to assert it. And since you can assert the same thing of x => 'A'
, you are given no actual type safefy improvement. So forget that.
So, that's what I have. Hope it's helpful; good luck!
Upvotes: 1
Reputation: 9113
First, we define:
type KeySetFor<T> = {
[key : string] : T;
}
This is a type that will encode the key names and what the resulting element type should be.
Then we define:
type KeyResultSetFor<T> = {
[key in keyof T] : T[key][]
}
This is the result type and what our function returns.
Now comes the tricky part, the function definition:
function groupByKeys<T, TKeySet extends KeySetFor<T>>(array : T[], possibleKeys : TKeySet, selector : (k : T) => string) : KeyResultSetFor<TKeySet>
And you call the function like this:
let arr = [] as AnyABC[];
let x = Arr.groupByKeys(arr, {
A : null as A,
B : null as B,
C : null as C
}, x => x.type);
This works, with the AutoComplete function of my IDE correctly displaying that x
has the members {A : A[], B : B[], C : C[]}
and the TypeScript compiled will correctly complain if I try to access x.D
for example.
However, there are some issues:
null
values. You could actually replace them with any other value as long as it's cast to the type A/B/C
. They're completely ignored. It's just very weird to see this in an API.D : null as C
in the keys object and it will work.x => "hello"
in the selector function and it will work, even though type
is of the type "A" | "B" | "C"
.Upvotes: 0