New mapped type's as clause allows one to filter the entries of an object. Basic mapped types preserve the shape of a tuple but not when used with the new as clause:
type tuple = [0, 1, 2]
type Mapped<T> = {
[K in keyof T]: T[K] | undefined
}
type MappedAs<T> = {
[K in keyof T as K]: T[K] | undefined
}
type test0 = Mapped<tuple> // tuple shape is preserved
type test1 = MappedAs<tuple> // tuple shape is object now
test0 === [0 | undefined, 1 | undefined, 2 | undefined]
test1 === {
[x: number]: 0 | 1 | 2 | undefined;
0: 0 | undefined;
1: 1 | undefined;
2: 2 | undefined;
length: 3 | undefined;
toString: (() => string) | undefined;
toLocaleString: (() => string) | undefined;
...
}
type FilterNumbers<T> = {
[K in keyof T as T[K] extends number ? never : K]: T[K] | undefined
}
type test2 = FilterNumbers<[1, '2', 3, '4']> // ['2', '4']
My suggestion meets these guidelines:
I believe this was a conscious design decision, and in this case I think compacting a tuple to a shorter length is pretty surprising to me. I would have expected a sparse array type in this case.
My use-case was for handling _ placeholders on ramda's curry. It allows one to pass a parameter's argument later if _ was passed as an argument. If _ is found then we keep its parameter's type for later, otherwise it gets consumed (filtered out). So this is to update the parameters after consumption, not sure if I can do this with a sparse array?
We could also have Pick and Omit for tuples thanks to this. An implementation of Pick and Omit that works for both objects and tuples.
This would be awesome if it could also preserve tuple labels after filtering. I got a version of curry that preserves tuple labels (from the curried function's params) by splitting up the individual parameters and then re-joining them after sorting - it's painful.
I kind of expect tuples/arrays to stay tuples/arrays if the as clause only produces numeric or number-like keys (I mean, K in keyof T as `get${K}` cannot reasonably stay a tuple/array, right?). If a numeric index in the middle is missing (so "0","1","3"), I expect a sparse tuple, not a shortened one. There's no way I can parse K in keyof T as Exclude<K, "2"> to somehow turn "3" into "2" without taking crazy pills first.
Now maybe if we suppress indices at the end it can produce a shortened tuple, since the as clause maps numeric keys in a straightforward way. So I'd be on board with preserving arrays/tuples in all cases where the resultant keys are numericlike, and making the output tuple have a length one larger than the largest mapped numeric key in the output type. Like this:
type KillTwo<T> = { [K in keyof T as Exclude<K, "2">]: T[K] };
type A = KillTwo<["a", "b", "c", "d"]>; // ["a", "b", undefined, "d"] or ["a", "b", never, "d"] or ??
type B = KillTwo<["a", "b", "c"]>; // ["a", "b"]
@jcalz Why not allow to delete entries, to create a real filter? That's how the current feature works with objects. On top of that, if we want the behavior you describe KillTwo could be written with:
type KillTwo<T> = { [K in keyof T]: K extends "2" ? never : T[K] };
This is the reason why I'm asking for as to serve as a filter, just like it does for objects. I want filtering capabilities because recursive types are heavy and don't preserve labels. On top of that, we could write Pick & Omit that work both on lists and objects - what is the point of getting an ugly object type if you Pick fields in a tuple?
If the tuple is shortened, the concerned keys are removed and the indexes recalculated.
Filtering keys would work on tuples, but do nothing on arrays:
type KillTwo<T> = { [K in keyof T as Exclude<K, "2">]: T[K] };
type test0 = KillTwo<[1, 2, 3]> // [1, 2]
type test1 = KillTwo<(1 | 2 | 3)[]> // (1 | 2 | 3)[]
Arrays don't have specific keys, Exclude<K, "2"> will always result in number for arrays
type Filter2<T> = { [K in keyof T as T[K] extends 2 ? never : K]: T[K] };
type test0 = Filter2<[1, 2, 3]> // [1, 3]
type test1 = Filter2<(1 | 2 | 3)[]> // (1 | 3)[]
Both array and tuple will match the condition T[K] extends 2, making filtering possible.
That's how the current feature works with objects
Does it, though? I think you and I must have different ideas on what is considered a filter. Here's an object:
type MyOmit<T, K extends PropertyKey> = { [P in keyof T as Exclude<P, K>]: T[P] };
type Hmm = MyOmit<{ zero: "a", one: "b", two: "c", three: "d" }, "two"> // { zero: "a"; one: "b"; three: "d"; }
I would be exceptionally surprised if what came out were { zero: "a"; one: "b"; two: "d" } and I'm guessing you would be also. But in my view this is exactly what you're asking for from this:
type AlsoHmm = MyOmit<["a", "b", "c", "d"], "2"> // ???
- If the tuple is shortened, the concerned keys are removed and the indexes recalculated.
I would love to be able to manipulate tuples at the type level, but on what grounds would you argue that as clauses should result in recalculating indices, as opposed to some other mechanism that would do this? I understand that it would be useful, but I don't see how to justify it.
but on what grounds would you argue that
asclauses should result in recalculating indices, as opposed to some other mechanism that would do this
I justified (to myself) that as was a good candidate for this feature because:
I want to see a standardization of the type system. A single way to deal with arrays/tuples/objects. One mapped type to rule them all.
I'm not opposed to having another mechanism at all, but I aim for simplicity. Now that we have as, I felt that because mapped types already preserve tuple/array shapes, why not mapped types with as clauses?
I don't see a case where we would like to see a tuple/array become an object. Most of the time, we would like to preserve its original shape.
In fact, I'm asking for better mapped types for tuples/array, and a standard way to write mapped types that work for all data types.
I find myself mostly agreeing with all those points, so maybe we're talking past each other? I'm going to try one more time and then stop:
I am in favor of as clauses mapping tuples to tuples and arrays to arrays, as long as numeric-like indices map to numeric-like indices in the clause. If that's all you were asking for I'd be πππ. I don't want to see a tuple become an object either.
But deleting numeric keys from an array does not logically result in a shorter filtered array; it results in a sparse array of the same length:
// as a mapped type
type FilterNumbers<T> = {
[K in keyof T as T[K] extends number ? never : K]: T[K];
}
// as a mapped array/object
function filterNumbers(t: { [k: string]: any }) {
const ret: { [k: string]: any } = Array.isArray(t) ? [] : {};
for (let k in t) (typeof t[k] === "number") ? (delete ret[k]) : (ret[k] = t[k]);
return ret;
}
console.log(filterNumbers({ one: "a", two: 2, three: "b", four: 3, five: "c" }));
// {one: "a", three: "b", five: "c"}
console.log(filterNumbers(["a", 2, "b", 3, "c"]));
// ["a", , "b", ,"c"]
The automatic reindexing you're talking about which turns a sparse array into a shorter array sounds like a great thing to have in the type system, but I just don't see how mapped types with as clauses should result in that.
It certainly can't be done by a straightforward reading of K in keyof T as F<K> where F<K> is always either K or never. That K is a real index from the keys of T. If it gets mapped to never, then that key is not present in the output type. If it gets mapped to K then that key is present in the output type. That's how it works with objects, and that's what I'd expect it to do with tuples also. I don't have much use for sparse tuples, but that's what I'd expect to see here.
It really feels like Anders gave us a screwdriver and we are now trying to use it to drive nails into a wall. But for that I want a hammer, not a screwdriver which auto-detects nails and sprouts a hammer head. Honestly, though, if such a screwdriver-with-optional-hammer-abilities were given to us, I'm sure I'd use it too. And maybe even be happy about it. But I'd sure feel awkward when explaining it to people on Stack Overflow.
Anyway, good luck with this feature request!
Most helpful comment
I find myself mostly agreeing with all those points, so maybe we're talking past each other? I'm going to try one more time and then stop:
I am in favor of
asclauses mapping tuples to tuples and arrays to arrays, as long as numeric-like indices map to numeric-like indices in the clause. If that's all you were asking for I'd be πππ. I don't want to see a tuple become an object either.But deleting numeric keys from an array does not logically result in a shorter filtered array; it results in a sparse array of the same length:
The automatic reindexing you're talking about which turns a sparse array into a shorter array sounds like a great thing to have in the type system, but I just don't see how mapped types with
asclauses should result in that.It certainly can't be done by a straightforward reading of
K in keyof T as F<K>whereF<K>is always eitherKornever. ThatKis a real index from the keys ofT. If it gets mapped tonever, then that key is not present in the output type. If it gets mapped toKthen that key is present in the output type. That's how it works with objects, and that's what I'd expect it to do with tuples also. I don't have much use for sparse tuples, but that's what I'd expect to see here.It really feels like Anders gave us a screwdriver and we are now trying to use it to drive nails into a wall. But for that I want a hammer, not a screwdriver which auto-detects nails and sprouts a hammer head. Honestly, though, if such a screwdriver-with-optional-hammer-abilities were given to us, I'm sure I'd use it too. And maybe even be happy about it. But I'd sure feel awkward when explaining it to people on Stack Overflow.
Anyway, good luck with this feature request!