-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Mapped type filtering - thoughts and discussion #17678
Comments
My feeling is that filtering on the right hand side is problematic since the visual distinction between applied types and type predicates is lost and it becomes awkward to project the right hand side. The following seems fairly clear type ObjectsOnly<T> = {[K in keyof T if IsObject<T[K]>]: Partial<T[K]>}; but the following reads strangely to me type ObjectsOnly<T> = {[K in keyof T]: Partial<IsObject<T[K]>>}; as does type ObjectsOnly<T> = {[K in keyof T]: IsObject<Partial<T[K]>>}; since I like the use of the |
@aluanhaddad I think, with experience and discipline, the RHS method could probably be acceptably readable, but I absolutely agree the LHS way is far better in that respect. RHS would rely very heavily on strong naming conventions. A LHS approach is definitely my preference. |
Regarding the use of type IsPrimitive<T> = true;
type IsPrimitive<T extends object> = false; and type HasToFixed<T> = false;
type HasToFixed<T extends {toFixed(): string}> = true; Then they could be composed as type IsPrimitiveWithToFixed<T> = IsPrimitive<T> && HasToFixed<T>; I'm not sure if that is a good direction... |
Yeah, nor am I. That composition example is interesting, but using types as values like that is really strange, and potentially a bit of a weird slippery slope. It feels like it's compromising something about the integrity of the language in a way. It's also worth noting that this becomes easy with TDOs: type And<A extends true, B extends true> = true;
type And<A extends boolean, B extends boolean> = false;
type IsPrimitiveWithToFixed<T> = And<IsPrimitive<T>, HasToFixed<T>>; |
I think there are two separate issues here.
type SuperType<boolean> = string;
type SuperType<string> = boolean; Which may have their uses, but I didn't really feel a need for them.
type Partial<T, PT> =
{
[P in keyof T if PT]?: T[P];
[P in keyof T if not PT]: T[P];
}
function trimBush<T, K extends keyof T if string>(obj: T, to_trim: K[]) : void;
type StringKeys<T> = keyof T if string; Personally, I don't care about conditional types, but I'd very much like to see conditional keyof implemented, because it would add so much. About deleting properties in mappings, I propose not doing it at all. Instead, type subtraction should be introduced (and with it, negative types, because type Without<T, TN> =
{
[P in keyof T if -TN]: T[P];
}
// Or, combined:
type WithAndWithout<T, TWith, TWithout> =
{
[P in keyof T if TWith - TWithout]: T[P];
} Obviously the subtraction part doesn't have to be implemented right away, but it's gotta come at some point, hopefully soon. The problem with deleting, that subtraction would solve, is that I don't think you should delete a property that matches T, if the property's type is T | X, instead the property's type should become X and only get removed if no type remains. interface A
{
a: boolean;
b: string | number;
c: string;
}
// See Without above.
type B = Without<A, string>;
// So B would be:
interface B
{
a: boolean;
b: number;
} Edit: Small change in the logic of negative types. |
I don't entirely follow this, sorry. Do you mean the declaration type overloading? Yes, that is a separate issue, that's what #17636 is. See that thread for the fact that it does actually have lots of uses. This issue is basically a spin-off from #17636 investigating specifically how mapped type filtering (conditional |
I'm sure it has valid uses, I guess I could have used them in a couple of places, but conditional keyof is much more valuable and I think it should be higher priority. Anyway, my point is that they're separate issues and they don't collide in any way when it comes to mapped type filtering, because that's a job for conditional keyof, while RHS filtering should never be a thing. Problems with RHS filtering:
It's basically a feature designed specifically to help you shoot yourself in the foot. I'd also like to bring into the discussion the syntax for conditional keyof. There were other issues before these two that discussed conditional mapping and another syntax that was suggested was the one in #13214: P in keyof T where T[P] extends number | string While I like the I'd go as far as suggesting the use of AND and OR operators in that query so you can do: P in keyof T where T[P] extends number | string and O[P] extends object |
This was briefly a gist, but now it's here with bonus typo (one was actually spelling, but I can pretend) corrections and
vanish
renamed todelete
.This arrogantly assumes the implementation of #17636 - declaration type overloading.
Aim of mapped-type filtering
Given some type
Foo
, construct a new typeBar
with a subset of the members ofFoo
, where that subset is determined by a type predicate. Both the key and type of the member should be available to the predicate. Type predicates should have the full power of the type system.This is analogous to
Array.prototype.filter
, where the index and item value are available to the predicate.Examples that should be achievable
Note that some of these may be achievable within the language today, albeit via round-about means. The aim is to have all of these achievable nicely.
How to do it?
It seems there are two basic ways that filtering could be implemented within the context of mapped-types.
A mapped-type looks something like this:
Where
A
andB
strike me as the two obvious sites where a filtering syntax would fit. I'm calling these LHS and RHS filtering respectively.LHS filtering
At first glance, this seems the obvious place to put the filter. An intuitive syntax for this may look something like:
Structurally this makes a lot of sense, and the for-in-if flow is nice. What to put in place of the ... is an issue, though.
Were a type declaration to be used here, some concept of meaningful return type would have to be introduced to type declarations.
Pseudo-value type predicates
Imagine the following example:
This actually reads really nicely. The downside to this is that we now have the compiler assigning meaning to the
true
andfalse
types. This brings them uncomfortably close to values, and is a strange direction to take the language.Another similar option would be to use truthiness and falsiness concepts here, perhaps any type other than
never
could be true. This suffers from most of the same issues as just usingtrue
andfalse
, though may lead to different (nicer?) predicate type declarations.Constraint-matching type predicates
Constraint matching could be used for the predicate. An example predicate may be defined and used like so:
The readability of this is pretty good.
Here the result of the predicate is irrelevant, the filtering is done according to whether or not
T[K]
satisfies the constraints of the predicate. This seems like a nice approach until you realise that this manner of defining predicates is significantly less powerful and expressive. You can specify as many cases as you like where it does match, but there's currently no syntax to specify a special case that doesn't match.One potential solution to this is subtraction-types.
Another solution may be to introduce a new type that I'll call
throw
for now. A type that resolves tothrow
would be considered a compile-error in most contexts, and would function the same as a constraint-match failure in a mapped-type.This
throw
approach is pretty weird, but compared to subtraction-types arguably leads to neater predicates.RHS filtering
Filtering in the RHS position initially makes a little less semantic/intuitive sense. One nice way to think about it may be that the type can be a normal type, or it can be nonexistant.
For this approach I'm proposing a new special type that I'm calling
delete
until I think of something better.delete
describes a member that does not exist.These two interfaces are exactly identical.
OnlyHasFoo1
does not have a memberbar
, it would not show up in intellisense orkeyof
results or anywhere else. There is nospoonbar
.This would allow filters to be written as follows:
This does not read as nicely as the LHS filter approach above, but in my opinion, this gels considerably better with the nature of the language as it currently exists. While
delete
is a fairly odd concept, it still describes the behaviour of the type, rather than being a pseudo-value astrue
andfalse
become in the LHS approach above.delete
may also find other uses. Perhaps declaration merging withdelete
could remove members? Or some complex type combinations would find a use fordelete
? That remains to be explored.The specifics of
delete
would need to be pinned down, particularly how it combines with other types.vanish | T
->delete
,vanish & T
->T
is where I'm at with that now, though that's not based on much. Interactions betweendelete
andnever
andany
would have to be worked out.This also poses the question of whether any non-existent member access should now have type
delete
, and what that may mean and what use it may have. Shouldtype Foo = {}["non-existent"]
now bedelete
?In summary / thoughts
I think some version of the constraint-matching type predicates solution would probably be my pick, but all of these have their pros and cons, and none is the clear leader in my eyes. There may of course be other better options I haven't even considered.
The text was updated successfully, but these errors were encountered: