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.
type Foo<T extends number> = {aNumber: number};
type Foo<T extends boolean> = {aBoolean: boolean};
type Foo<T extends any> = {aDefault: T};
type FooNumber = Foo<number>; // = {aNumber: number};
type FooString = Foo<string>; // = {aDefault: string};
This idea was spawned in #17325, credit to @SimonMeskens for wanting to discuss it properly and pushing me to post it.
Potential problems: Does this clash with declaration merging somewhere?
@TheOtherSamP
Potential problems: Does this clash with declaration merging somewhere?
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 any
to take precedence and subsume the other constituents of the overloaded type.
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 https://github.com/Microsoft/TypeScript/issues/6606#issuecomment-317664772:
overloading covers:
promised
not covered is function application:
compose
, curry
, bind
, reduce
, filter
, tuple map
, ...)Going by my list in #16392:
ReturnType
, BoolToString
, Matches
/ TypesEq
/ InstanceOf
, ObjectHasElem
, TupleHasElem
, conditional errors, pattern matching, constraints, ObjectHasStringIndex
, ObjectHasNumberIndex
.map
over tuples / heterogeneous objects, reduce
, filtering objects by predicatetl;dr:
null
/ undefined
case #17370), extracting return types (naively) / params from unapplied functions.isT<T, V>
into a isBool<V>
, can't think of use-cases that need this atm, but fixable with anonymous types (not aware of proposals).Thanks @tycho01, that list is really great to have and makes a far better case for this than I could have on my own.
If that problem exists for functions already though, that means it would also exist doing this kind of type switching via #6606.
The existing declare function foo(v: string): bar; declare function foo(v: number): baz;
style overloads would get their return types for given inputs calculated with #6606, yeah.
For the type-level use-cases utilizing it for overload-based type checks / pattern matching though, an type-level approach like interface foo { (v: string): bar; (v: number): baz; }
would seem more idiomatic though, to prevent excessive switching between expression/type levels.
In that sense, I think the clashing issue is not so much aggravated by 6606, as it doesn't so much encourage repeating names.
But yeah, can't blame this proposal either if it was already an issue beforehand.
For the type-level use-cases utilizing it for overload-based type checks / pattern matching though, an type-level approach like interface foo { (v: string): bar; (v: number): baz; } would seem more idiomatic though, to prevent excessive switching between expression/type levels.
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.
Then again, that risky scenario is already seems to be where we are for functions now, I guess?
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 typeof
) be applied to function types to generate matching (anonymous) type declarations? So rather than full #6606, typeof (<T>(o: T) => {item: T})
would become type <T> = {item: T}
. I haven't really thought this through at _all_ yet, just throwing it out really.
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
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.
That sounds a lot like the Variadic Kinds proposal https://github.com/Microsoft/TypeScript/issues/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.
That sounds a lot like the Variadic Kinds proposal #5453
I think I was wrong there actually, that shouldn't be required. A single type parameter should be able to handle the rest parameter.
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, (a: A, b: B, cs: number[])
could be called as a type as <[MyA, MyB, [MyC1, MyC1]]>
.
Could some operator (perhaps
typeof
) be applied to function types to generate matching (anonymous) type declarations? So rather than full #6606,typeof (<T>(o: T) => {item: T})
would becometype <T> = {item: T}
.
That type <T> = {item: T}
notation sounds like the anonymous / unapplied types add-on I mentioned.
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.
the size of #6606
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).
@SimonMeskens
interface Fizz<T> { <T extends number>bar: {aNumber: number}; <T extends boolean>bar: {aBoolean: boolean}; <T extends any>bar: {aDefault: T}; }
Hmm, that's interesting. It seems to me, those properties aren't actually generic, they're just using the <T extends number>
as a filter? My first instinct is that's a little uncomfortable because it's a different use for the same syntax, but I'm not sure.
more idiomatic
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>;
}
@tycho01
(a: A, b: B, cs: number[])
could be called as a type as<[MyA, MyB, [MyC1, MyC1]]>
In the case of rest parameters typeof ( <T>(...args: T) => T[] )
, I was thinking the type argument would just still be T
, so the produced type would be type <T> = T[]
. The tuple is interesting, but I can't quite get my head around exactly how that would work with varying numbers of arguments. Why [MyC1, MyC2]
rather than MyC1 | MyC2
?
That type
= {item: T} notation sounds like the anonymous / unapplied types add-on I mentioned.
Yep, I pretty much stole that from you. I'm shameless.
the size of #6606
It's just ()! 馃槂
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 <T extends number>
that'd currently create a new generic, with the same name yet distinct from that top-level T
. afaik properties (aside from functions) currently couldn't have generics either.
In the case of rest parameters
typeof ( <T>(...args: T) => T[] )
, I was thinking the type argument would just still beT
, so the produced type would be type<T> = T[]
.
Heh, I'd been interpreting things such that the whole type array would be captured into T
this way, but yeah whatever, doesn't matter much here.
Why
[MyC1, MyC2]
rather thanMyC1 | MyC2
?
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.
I want union iteration myself as well, but don't know of / haven't made proposals so far; not sure what'd be most elegant. I mentioned it a few times in #16392; best thing I could come up with was proposing some union-to-tuple operator. Open to ideas.
what about something like this?
Well, I certainly hadn't thought of that. 馃槄
Heh, I'd been interpreting things such that the whole type array would be captured into T this way, but yeah whatever, doesn't matter much here.
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.
T
I'd just reason (...args: any[])
-> (...args: T)
-> T
must be some array. But yeah whatever.
Are we ready for a separate issue for this stuff?
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 Object.entries
which, while useful, are poorly designed even in the context of a purely dynamic language.
For example, the shape of Object.entries
seems intended and optimized for concise consumption with implicit renaming as in
for (const [key, registration] of Object.entries(registry)) {
yield {key, registration};
}
But I think the pattern is degenerate, that Object.entries
should return an Iterable<{key, value}>
.
After all, how much harder is it to write
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 https://github.com/gcanti/typelevel-ts/issues/8#issuecomment-318225755:
A very large part of the useful abstractions in TypeScript end up being hacky mapped types.
Hopefully at one point we'd have pretty solutions for everything.
On tuple iteration specifically, my initial use-case for it was Ramda's path
. At the time, even with the silly type Inc = [1, 2, 3]; // ...
thing, that hacky construct to iterate over them just felt like a nicer idea than using 1000-line overloads for inferior functionality (no tuple support) and terrible performance.
That still doesn't work in functions, but yeah.
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 { [K in keyof Foo]: If<Matches<T[K], string | number>, T[K], never> }
, never
s get pruned automatically.
Like { [K in keyof Foo]: If
, T[K], never> }, nevers get pruned automatically.
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 never
, which this would seemingly prevent.
Anonymous type declarations now have an issue at #17640.
Function types -> anonymous type declarations now have an issue at #17641. Any help with the title of that issue would be appreciated as it's a mess right now, not sure how to express that concept succinctly.
I think I'm in favor of only allowing anonymous type declaration overloads, not the ones in the example in the OP, to alleviate the awkwardness;
Here's how this would work:
Allowed:
type Foo<T> = {
value: (
type <U extends number> = {aNumber: number};
type <U> = {aDefault: U};
)<T>
};
Allowed:
type Foo<T> = (
type <U extends number> = {aNumber: number};
type <U> = {aDefault: U};
)<T>;
Not Allowed:
type Foo<T extends number> = {aNumber: number};
type Foo<T extends boolean> = {aBoolean: boolean};
type Foo<T extends any> = {aDefault: T};
This would also help with the declaration merging issue?
I would replace the example in the OP with this second example, personally
@SimonMeskens
Oh hmm, that's interesting.
I don't think I share your view that the full type overloading is awkward, I actually like the syntax, and how it mirrors that of functions. This _would_, however, eliminate the merging/clashing issue, which is a point in favour.
Could you expand a little on what you think is awkward about the main proposal here?
Well, the issue I have is that now, if you accidentally have a name clash for a type, the compiler will complain. With this proposal, certain name clashes will silently get swallowed and treated as overloads.
Having just the anonymous versions fixes both declaration merging and that issue. Syntax-wise, it also provides some sort of security blanket, as the different statements are not just grouped by the compiler, but also visually, with the brackets.
Does that make sense?
@SimonMeskens Yeah, that makes sense. I think the scenario where identically named types are accidentally placed next to each other, forming overloads when you'd want an error, are pretty rare though.
There's something else that overloads get us that we haven't really discussed here, that restricting them to anonymous types would take away - varying numbers of type parameters.
type AnyOf<T1, T2> = T1 | T2;
type AnyOf<T1, T2, T3> = T1 | T2 | T3;
Obviously that's just a little toy example, but it could be nice to define named type declarations like that where overloading is really used in the same way it is in functions - to group a set of operations under a single name and differentiate by signature. As far as I can tell, restricting overloading to an anonymous context would take this use away.
But I _am_ still a bit uncomfortable about the clashing issue.
One option in many linters is to force the use of brackets in if statements. The reason being that counting on the programmer to maintain adjacency and indentation is error-prone. Introducing a language feature that not only encourages, but relies on adjacency feels a bit iffy. Mind you, this is not a huge roadblock to me, I'd be happy to take the expressiveness of this feature, whichever way it comes :)
Besides, overloads and declaration merging already have a weaker version of this issue of adjacency and we accept that.
This is a great idea, and allows for a lot of concepts to be expressed that are difficult to accomplish otherwise. In TypeScript, I can't think of a way to express the signature of a function that takes an arbitrarily nested array of some type and produces another array. With the equivalent Haskell feature, known as "type families", I can do:
type family Flattened a :: * where
Flattened [[a]] = Flattened [a]
Flattened a = [a]
flatten :: a -> Flattened a
--- :t flatten [[[1]]]
--- -> [Int]
It would be nice if I could similarly do:
type Flattened<A extends B[], B> = Flattened<B>
type Flattened<A> = A[]
const flatten: <A>(a: A) => Flattened<A>;
// typeof flatten([[[1]]])
// -> number[]
@masaeedu
If we also had #14400, or some other way to capture the type of B
, this would work. That raises the point that it's not clear how well #14400 plays with this proposal when you've got overloads of different lengths, that needs some thought.
Essentially, what this highlights is the need to be able to pull generic types out of other types, such that given type A<T extends B[]>
we can access B. Whether that's by #14400 or some other syntax is open. I think it's worth noting that that's not necessarily exactly relevant to this specific issue (though it is to the larger discussion), as it would stack nicely on top of this.
However, I actually think this particular example may well be achievable within the language as it stands today, albeit using a fairly weird/hacky approach. I just published a gist showing off a weird trick that allows rudimentary type switching with interface augmentation, and I think that maybe could be extended to allow this. I imagine the point you're raising is a little more general than the scope of that trick, but I'm going to have a play around with it and see if it works anyway.
@TheOtherSamP:
Are you saying that's already in the language? I wasn't aware of that, and a quick test failed to recreate it.
I seem to have misremembered; I guess we made objects containing keys or never
, use keyof
to just get the keys without never
, then plug the keys back into the original object so as to filter it. Source https://github.com/Microsoft/TypeScript/pull/13470#issuecomment-307344395.
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.
I think the scenario where identically named types are accidentally placed next to each other, forming overloads when you'd want an error, are pretty rare though.
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.
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.
Dunno if that could be further simplified.
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 [K in keyof T if ...]
feature to be added later, and use your more hardcore methods in the meantime.
not exactly approachable
Yeah, the mainstream approach is just Partial<Foo>
.
I think if we can make this syntax nicely handle filtering without having to do all that, that would be great.
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 x/a.d.ts
and y/a.d.ts
so which one do you think will take precedence?
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 never
? Working title, vanish
.
A property of type vanish
would, well, vanish. It wouldn't exist. So Foo
and Bar
are identical here.
interface Foo {
foo: number;
bar: vanish;
}
interface Bar {
foo: number;
}
Such that a filter can simply map a property to type vanish
to remove it.
Initially I'm thinking vanish | T
-> vanish
, vanish & T
-> T
, so the opposite of never
. A type with these semantics might also be nice for the more advanced typing tricks, though that's a little besides the point.
@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 vanish
would just be T - T
with subtraction types, unless I'm misunderstanding something.
@masaeedu Ahh, I think I probably explained it badly, sorry. vanish
would completely remove the property. It's a bit weird I know, but a property of type vanish
just wouldn't exist.
I misremembered how subtraction types were proposed (see #4183). I was assuming T - T
would be never
, which is a type that admits no values (and hence seems like it would be identical to vanish
). Apparently not though.
@masaeedu never
isn't identical to vanish
.
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 https://github.com/Microsoft/TypeScript/pull/21316 ?
@laughinghan yeah, seems like it.
Most helpful comment
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.