Repository navigation
Support locally scoped type alias nodes #23188
Description
We mentioned this at a previous design meeting while discussing conditional types but I don't think an issue was opened for it (nor can I find mention of it within some design meeting notes). We think it'd be useful for both conditional types (to make a bare type reference/parameter) and in complex types (to reduce duplication) to enable a kind of type-alias-as-a-type-node syntax. Something that allows rewriting code like this:
type Foo<T, K extends string, TVal extends T[K] = T[K]> = TVal extends string ? {x: TVal} : never;as
type Foo<T, K extends string> = type TVal = T[K] in TVal extends string ? {x: TVal} : never;or
type MyBox<T, K extends keyof T = keyof T, TVal extends T[K] = T[K]> = {
host: T,
getValue(k: K): TVal,
setValue(k: K, v: TVal): TVal
};as
type MyBox<T> = type K = keyof T, TVal = T[K] in {
host: T,
getValue(k: K): TVal,
setValue(k: K, v: TVal): TVal
};this allows elision of unnecessary information (no need to write both a constraint and identical default), and prevents usages from accidentally providing an extra type parameter when they shouldn't (because the defaulted parameter was used effectively as a const). I believe Brian Terlson (@bterlson) had mentioned wanting something like this in-person, too.
As for proposed syntax, I'd say:
- A
LocalTypeAliasis aTypeNode(so is valid anywhere aTypeNodeis). - A
LocalTypeAliasis parsed as a requiredtypekeword, then a comma separated list ofLocalTypeAliasAssignments (with at least one element) - the type assignment list, followed byinand an arbitraryTypeNode- the subject of the assignments. - A
LocalTypeAliasAssignmentis anIdentifierfollowed by=followed by aTypeNode. (does this need to be restricted to parse in a human-understandable way?)
For semantics:
- A
LocalTypeAliasis a local scope around its subject type node. It binds a declaration for a type parameter for each identifier in each assignment in its type alias assignment list, constrained to the assigned value, and establishes aTypeMapperto map those type parameters to their assigned value as well (similar to an instantiated generic type alias). (Should they be allowed to be mutually referential? Other parameter lists are, so I don't see why not.) These aliases are made to be type parameters (and not raw aliases like a nongeneric type alias declaration) so they interact favorably with conditional types - eg, they are a way to force a conditional type to iterate over a union for any type node without introducing another top-level alias.
Thoughts?
Activity
kpdonn commentedon Apr 6, 2018
I think it's a great idea. I've wanted to propose something like this before but I wasn't able to imagine a workable syntax.
I do think the syntax is a little hard to follow but I'd be willing to accept that in exchange for the benefits you mentioned. I see myself using this all the time.
kpdonn commentedon Apr 6, 2018
I think the difficulties in reading it largely come from two places:
=for theLocalTypeAliasAssignments appear without any visual nesting to separate them from the initial=for the top-level alias.inessentially has to replace the meaning that=has in regular type aliases that don't need anyLocalTypeAlias.You've opened my mind to the possibilities though so if you're open to some bikeshedding, what about:
and
It allows visual nesting between the top level
=and the=for theLocalTypeAliasAssignments and it keeps the meaning of the top level=the same for both type aliases that needLocalTypeAliasand those that don't.weswigham commentedon Apr 6, 2018
Kevin Donnelly (@kpdonn) I'd like it to be a type node and not an alias variant, so it can be used with embedded conditional types, eg
This way it can always be used to force a conditional type to distribute over unions.
kpdonn commentedon Apr 6, 2018
Okay I can see that, that's even more powerful than I was thinking.
I do have a hard time with really wanting to read the
infromtype Val = T[K] in Valas somehow similar to theinfrom[K in keyof T]even though as far as I can tell they are conceptually unrelated, but I'm sure I could get used to it.Edit: Maybe
forinstead ofin? That seems to read more naturally to me anyway.ajafff commentedon Apr 6, 2018
I like the
withproposal. I actually wanted to suggest something similar:IMO this makes it more readable and may already look familiar due to the similarities to JS with statements (don't know if that's desirable).
weswigham commentedon Apr 6, 2018
What you have there looks like a block or an object type. If we're drawing analogies to value spaces we'd want something that works like an expression. Think let expressions in Haskell.
jack-williams commentedon Apr 6, 2018
Am I right in thinking that semantically the proposal is much like an anonymous type abstraction that is immediately applied? Similar to how
letcan be encoded at the value level with:let x = M in N===(x => N) Mthen the type level version is:
type X = A in B===(<X>B)<A>This would be consistent with local aliases causing distribution.
I think the main concern with the syntax would be users getting confused between these two:
Is there a reason why
letis off the table?kpdonn commentedon Apr 6, 2018
Thinking more about my personal readability difficulties, I keep having problems with
inbeing the delimiter. I'm not able to see the where the end of theLocalTypeAliasAssignmentlist is at a glance. And even once I find it my eyes keep wanting to mentally bind theinto just the very last token. Adding parentheses to show how I am mentally grouping it, I keep trying to read it likeor
instead of the correct
Another syntax suggestion
Thinking about that plus Jack Williams (@jack-williams) concern about the different meanings of
type T = number | stringdepending on the location, what about changing the definition for parsingLocalTypeAliastoLocalTypeAliasis parsed as a requiredtypekeyword, then an opening<, then a comma separated list ofLocalTypeAliasAssignments (with at least one element) - the type assignment list, followed by a closing>and then an arbitraryTypeNode- the subject of the assignments.Examples
Thoughts
That syntax resolves my readability difficulties. It doesn't seem like there would need to be any extra delimiter after the
>such asinorforor=. I'm actually not even certain thetypekeyword is really needed but I left it there.I think that syntax would also help with Jack Williams (@jack-williams) comment by making it more obvious that the
LocalTypeAliass are type parameters because they'd be declared inside<>brackets the same way other type parameters are. So I think it'd be more clear thatis analogous to
and not analogous to
Finally
Just want to re-emphasize that I like the proposal a lot regardless of syntax. So if you don't think my suggestion is an improvement then I'd still be very happy with the original.
cefn commentedon Aug 22, 2020
Thanks to Jack Williams (@jack-williams) for pointing me to the correct issue. I closed #40194 in favour of this.
I found myself thinking of it as a conceptual combination of Mapped Types and explicit distributive logic. If this is correct, the syntax proposal I sketched using both
inandeachmay help. It leads to type declarations like ...The problem case I mentioned in the original issue would therefore read like...
If I followed it correctly, the OP's problem case...
...would look like...
...or limiting to the constrained, mapped type, union case
K in keyof...DanielRosenwasser commentedon Nov 11, 2020
Related is #41470 which thinks about this in terms of an anonymous namespace so that you can have arbitrary locals.
xuld commentedon Jan 23, 2025
I'm suggesting an alternative syntax for locally scoped type alias:
With two locals:
This syntax does not introduce new keywords, and is compatible with jsx as the
=token is not valid in jsx.When using
in, which can also make locals distributive:Currently, types are distributive only in conditional types , forcing users to write meaningless conditional types like
T extends any ? T : never.