-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
Add type declaration overloading #17636
Comments
Potential problems: Does this clash with declaration merging somewhere? |
@TheOtherSamP
I think so. Even if we assume modules, Module Augmentations would make ordering perplexing. If you have // vendor/api.d.ts
export type Foo<T extends number> = {aNumber: number};
export type Foo<T extends boolean> = {aNumber: boolean};
export type FooNumber = Foo<number>; // = {aNumber: number};
export type FooString = Foo<string>; // = {aDefault: string}; and then add // src/augmentations.d.ts
export {}
declare module 'vendor/api' {
type Foo<T extends any> = {aDefault: T};
} then ordering could be violated causing I might be wrong on this... |
@aluanhaddad Hmm, why isn't this already a problem for function overloading? Actually, it seems like it is? I just threw a quick test together of the same thing with functions and it seems like it does add the overload, but it's not clear how the overload resolution is working. Is there some reason this should be more of a problem than it already is with functions? |
@TheOtherSamP I am actually suggesting that overloaded functions suffer from the same issues. This proposal does seem very useful though. |
@aluanhaddad Oh right, okay. Yeah, it's interesting that that problem is pretty much baked into the language now then, I don't see that there's much that could be done to fix that without massive breaking changes. If that problem exists for functions already though, that means it would also exist doing this kind of type switching via #6606. |
@TheOtherSamP asked me to give my view on how this compares to #6606, another proposal that would enable overloading, be it through type-level application of overloaded functions. Going by my list in #6606 (comment): overloading covers:
not covered is function application:
Going by my list in #16392:
tl;dr:
|
Thanks @tycho01, that list is really great to have and makes a far better case for this than I could have on my own. |
The existing For the type-level use-cases utilizing it for overload-based type checks / pattern matching though, an type-level approach like In that sense, I think the clashing issue is not so much aggravated by 6606, as it doesn't so much encourage repeating names. |
Good point. I suppose the argument could be made that we shouldn't add another place where the clashing issue manifests. I don't want to make that argument because I like this idea, but it does feel a bit wrong to put this in knowing that it's broken. |
Erroring on clashes is an effective way to deal with accidental clashes, yeah. Not sure about nice workarounds there. So this proposal: if a generic input fails to match, find overloads. I presume this to mirror how function overloads work now, picking the first that matches by input requirement, rather than also continuing if say return type calculations explode. If the foreign overload is broader than yours or goes first, it might catch (some of) your cases. If it catches all, that could warrant a "warning, given these equal requirements things could never drop through to the second declaration". Some would be harder to disambiguate. |
Very tired left-field thought: Could some operator (perhaps I'm thinking that it might be a way to avoid introducing the new syntax/concepts that #6606 is waiting on, and that overloaded type declarations should be capable of describing everything that function signatures can. |
Although rest parameters might be a problem. I have a horrible feeling that may also need rest parameters in type declarations, and I'm not 100% sure what that would mean right now. It's also suddenly becoming a fairly hefty set of changes when you include that, but maybe that's acceptable when compared to the size of #6606 as it stands. EDIT: Actually I think that wouldn't be an issue. I think you should only need a single type parameter for the rest parameter. |
@TheOtherSamP
That sounds a lot like the Variadic Kinds proposal #5453 |
@aluanhaddad I think I was wrong there actually, that shouldn't be required. A single type parameter should be able to handle the rest parameter. I am, however, falling asleep and probably embarrassing myself by talking nonsense at this point, so who knows. |
Pretty much, yeah, you can fake variadic generics by cramming stuff into tuples. They were suggested there anyway, though I'm with you we could probably do without. Say,
That I guess you could do it given this #17636 + those anynomous / unapplied types + 5453 (bonus if incl. variadic generics) + a way to extract params (#14400 / 6606) + some trick to get the proper return types (like 6606 yet not 6606), yeah.
It's just |
So there's a little awkwardness here in introducing syntax that needs to be in a certain place (all have to be adjacent or similar concerns). This is unavoidable due to how the proposal works. So I'm wondering if we couldn't propose a weaker version without these issues, that has equivalent power. What if we propose instead something like property overloading: interface Fizz<T> {
<T extends number>bar: {aNumber: number};
<T extends boolean>bar: {aBoolean: boolean};
<T extends any>bar: {aDefault: T};
} This should be exactly equivalent to the proposal, if this also works: type Foo<T> = Fizz<T>["bar"]; This looks somewhat more idiomatic, it alleviates the awkwardness, but doesn't fix the declaration merging issue (which is less of an issue as noted above). |
Hmm, that's interesting. It seems to me, those properties aren't actually generic, they're just using the
Personally, I'm not sure I agree. Type declarations feel like the right place to be manipulating types in this way, properties on interfaces like this, or functions in #6606, feel like a hacky abuse of a language feature. For comparison, the type declaration equivalent of that interface would be: type Foo<T extends number> = {aNumber: number};
type Foo<T extends boolean> = {aBoolean: boolean};
type Foo<T> = {aDefault: T};
interface Fizz<T> {
bar: Foo<T>;
} |
In the case of rest parameters
Yep, I pretty much stole that from you. I'm shameless.
Yep! I actually meant that we're stacking up quite a few proposals at once here, but maybe the total burden of these is still not too large compared to the burden of doing #6606 instead. What I like about this, is that it can actually be done nicely in parts. Compared to #6606, this could be nice as we might not have to wait for every part of this to happen before we can start enjoying some of the benefits.
|
Regarding anonymous types (which might need their own issue soon), what about something like this? type TypeWithInlineStuff<T> = {value: (
type <U extends number> = {aNumber: number};
type <U> = {aDefault: U};
)<T>}; I was primarily suggesting them to allow for the function type conversion, but it would be nice if they could also solve the inlining problem like this too. |
interface Fizz<T> {
<T extends number>bar: {aNumber: number};
<T extends boolean>bar: {aBoolean: boolean};
<T extends any>bar: {aDefault: T};
} In my interpretation when you do
Heh, I'd been interpreting things such that the whole type array would be captured into
In the first place because I know how to iterate over tuples types so as to do stuff with them, but don't see a straight-forward way using union types.
Well, I certainly hadn't thought of that. 😅 |
I'm still not quite getting how this would work, as the function isn't being called so we don't have the arguments. I may be being thick. I think this is a relatively unimportant detail of a side issue for now though, probably don't need to dwell on it yet. Are we ready for a separate issue for this stuff? Maybe even two, one for anonymous type declarations, one for function types -> anonymous type declarations? I could go ahead and make those, I'm not sure if that's neater than keeping that discussion here, or prematurely fractures the discussion. |
I'd just reason
Works for me, if you mention thread numbers here I'll subscribe. |
I'm still largely of the mind that iterating over tuples is an unpleasant consequence of their concrete manifestation as (H)Arrays in JavaScript and how that has to be interpreted by TypeScript in order to make any sense at all out of the intended use patterns of API's like For example, the shape of for (const [key, registration] of Object.entries(registry)) {
yield {key, registration};
} But I think the pattern is degenerate, that for (const {key, value: registration} of Object.entries(registry)) {
yield {key, registration};
} Of course I'm speaking entirely about value level and not type level constructs but I think that the former has corrupted the latter such that we wish to iterate over tuples and can do so in a roughly typed manner as a consequence of TS having to model standard JS APIs that are awkwardly designed. |
@aluanhaddad: I'll grant you that. Reminds me a bit of a recent quote from gcanti/typelevel-ts#8 (comment):
Hopefully at one point we'd have pretty solutions for everything. On tuple iteration specifically, my initial use-case for it was Ramda's If tomorrow the most powerful constructs to type things are different from today, then yay, progress. |
I'm working on new issues for anonymous type declarations and function types -> anonymous type declarations now. Just a thought though, while this gets us type switching, we don't quite have type filtering here just yet. How would we selectively map over a type with this? Should a type with no matches map to nothing in a mapped type? interface Foo {
name: string;
age: number;
isEnabled: boolean;
}
type StringNumberMapper<T extends string | number> = T;
type Bar = { [K in keyof Foo]: StringNumberMapper<Foo[K]> }; // = { name: string; age: number; } |
Like |
Are you saying that's already in the language? I wasn't aware of that, and a quick test failed to recreate it. If you're suggesting that be added, I think I like that. The only question I'd have is whether you'd ever legitimately want to produce an unpruned |
@TheOtherSamP:
I seem to have misremembered; I guess we made objects containing keys or I guess that'd make it something like this instead: type ObjectValsToUnion<O> = O[keyof O];
Pick<Foo, ObjectValsToUnion<{ [K in keyof Foo]: If<Matches<T[K], string | number>, K, never> }>> Dunno if that could be further simplified.
That's a fair point; so far I hadn't considered it should only consider them overloads if next to one another. It might give a bit of extra complexity for implementation, but it does address the concern. |
Oh, apologies, I thought that was clear. I was imagining adjacency would be required in the same way it is for function overloads. It's not quite the same thing, but it seemed analogous. That doesn't actually eliminate the merging problem though, as with functions those still become overloads.
I think if we can make this syntax nicely handle filtering without having to do all that, that would be great. That stuff is fine for power-users, but not exactly approachable to someone just getting started. This feels like it might be an opportunity to add filtering really smoothly, though I'm not sure how. Of course the other option is to just leave that alone and wait on some |
Yeah, the mainstream approach is just
Well, this is why I started a type library, so people wouldn't need to reinvent the wheel. Potential abstractions given language add-ons: type ObjectValsToUnion<O> = O[keyof O];
// 6606
type Filter<T, Cond> = Pick<T, ObjectValsToUnion<{ [K in keyof Foo]: If<Cond(T[K]), K, never> }>>;
type Bar = Filter<Foo, isT<string | number>>;
// anonymous types, syntax made up on the spot, not necessarily overload-friendly
type Filter<T, Cond> = Pick<T, ObjectValsToUnion<{ [K in keyof Foo]: If<Cond<T[K]>, K, never> }>>;
type Bar = Filter<Foo, <V> => Matches<V, string | number>>; |
it doesn't seem that anyone noticed that currently there is NO SPECIFIED ORDER in which type declarations from different files are applied, say you have or you have // a.d.ts
declare global {
type T = whatever;
} and then somewhere else // a.d.ts
declare global {
type T = meh;
} |
@Aleksey-Bykov Relevant discussion here. |
there is a standing problem of making function/method overloads from different definitions work consistently, i don't think this proposal will get anywhere before that problem is fixed |
@Aleksey-Bykov Except this thread already contains potential fixes and workarounds for exactly that issue? |
While we'd talked about this already existing for methods here, somehow we hadn't noticed that this problem already exists for type declarations too, so thanks for pointing that out @Aleksey-Bykov. Actually though, my assumption is that that might make it less of an issue for this proposal. If type declarations are already broken in that manner, this proposal really doesn't make things any worse than they already are. The only way I can see it being a blocking issue for this is if you think we shouldn't do anything with type declarations at all until it's fixed. Or, I suppose, if you think having to work around the existence of overloads could make the merging issue harder to solve. However that's a problem that already needs to be solved for functions, so I don't see that that could be too much of an issue. |
Random just-woke-up thought regarding filtering, what if we introduced another special-case type like A property of type interface Foo {
foo: number;
bar: vanish;
}
interface Bar {
foo: number;
} Such that a filter can simply map a property to type Initially I'm thinking |
@TheOtherSamP There is an issue tracking subtraction types in a different place, I think that is more of a cross-cutting concern in the language. Doesn't seem particularly relevant to this issue. |
@masaeedu Well, the (admittedly weak) relevance to this issue is that it might play nicely with the type of type switching this enables. For example: type FunctionFilter<T extends Function> = vanish;
type FunctionFilter<T> = T;
type NonFunctionMembers<T> = {[K in keyof T]: FunctionFilter<T[K]>}; It's not immediately apparent to me how the same thing could nicely be implemented with subtraction types. |
It seems your |
@masaeedu Ahh, I think I probably explained it badly, sorry. |
I misremembered how subtraction types were proposed (see #4183). I was assuming |
@masaeedu type Foo = keyof {bar: never, fizz: "bang"}; // = "bar " | "fizz"
type Foo = keyof {bar: vanish, fizz: "bang"}; // = "fizz" I was thinking about this a bit last night, I think I'm going to write up a gist with my thoughts on the topic of type filtering because I want to get my thoughts down on (digital) paper, but I don't want to derail this thread too much. I'll link that here if I get it done. |
@masaeedu I got a little carried away with thinking about the filtering, the gist is here. I don't want to put up yet another issue just yet (I feel I've spammed them a little recently) but I may put that up for discussion at some point. If anybody particularly wanted to discuss anything there now I could be persuaded to make it into an issue. I feel that topic is tangentially related to this one as a lot of the discussion relies on type declaration overloading, but it's not exactly a discussion about type declaration overloading. I do think it demonstrates some of the power of TDO though. |
That gist is now an issue at #17678 because I have no chill. |
I finally got a PoC for the overload pattern matching working now at #17961. So that's using type level function application, but feel free to try if interested. |
Isn't this resolved by #21316 ? |
@laughinghan yeah, seems like it. |
I'm keeping this relatively brief and loose for now as I haven't thought it through in excruciating detail yet, I may add detail later to flesh this out into more of a formal proposal.
There's a desire to have some form of type switching in the type system. I think #12424 is the main proposal that tackles this. This suggestion is yet another potential approach to that issue, as well as potentially adding some other little niceties to the type system.
If type declarations could be overloaded in much the same manner as functions, this would allow for type switching in a manner that's already familiar within the language. The rules would effectively be the same as that of functions.
This idea was spawned in #17325, credit to @SimonMeskens for wanting to discuss it properly and pushing me to post it.
The text was updated successfully, but these errors were encountered: