Skip to content
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

Inline type aliases #30979

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Apr 16, 2019

This introduces a new typenode with the syntax:

type Identifier = TypeNode

where within the child TypeNode, Identifier is seen as an in-scope reference to the type represented by TypeNode. This mostly reuses out existing type alias machinery (with no parse support for generics). This is used along the lines of:

interface Container {
  invoke: type TInvoke = () => TInvoke;
}

This allows anonymous self-referential types to rounttrip thru declaration emit. I'll be spending a bit collecting the related usecases and beefing up the tests, but the implementation is largely done, so I wanted to open a PR to 🚲 🏠 the syntax on. Notable discussion points include:

  • What priority should it parse at? (currently the same as a TypeOperator)
  • Should a sigil other than = be used?
  • Should a keyword other than type be used?
  • Should the syntax be expanded to allow mutually recursive aliases and/or in context expressions, akin to a recursive let?

@weswigham
Copy link
Member Author

cc @DanielRosenwasser @uniqueiniquity and @RyanCavanaugh who briefly got to preview this earlier.

@ahejlsberg
Copy link
Member

Without rendering an overall opinion, how about is instread of =? So:

interface Container {
  invoke: type TInvoke is () => TInvoke;
}

@weswigham
Copy link
Member Author

I personally feel like that's a bit wordy, since a series of 3 or more non-sigil tokens starts looking more like prose than code; but that's just my own opinion - it might be that that does have nicer aesthetics. (Likewise multiple consecutive sigils is too symbolic; a alternating sequence is what usually looks normal to me)

@DanielRosenwasser
Copy link
Member

Yeah, I'd also prefer to keep it consistent with type alias declarations unless doing so would trample on likely future syntax.

@RyanCavanaugh
Copy link
Member

Search term: Named type expression

@SrBrahma
Copy link

+1

I come from #40780. Even this not being the exact the syntax I proposed days ago, I still think this is very necessary for a better DX in more advanced types definitions. Any updates on this?

@rbuckton
Copy link
Member

I considered something similar several years ago, but using type T = U, V, somewhat similar to a let..in-like expression. That would allow you to:

  1. Introduce multiple intermediate types scoped to the type
  2. Control the specificity of your final type result

So, with the syntax I was thinking, the example in the OP might read:

interface Container {
  invoke: type TInvoke = () => TInvoke, TInvoke;
}

But also allows for more complex types like this:

// before (uses a type default to define `K`)
type MatchingKeys<TRecord, TMatch, K extends keyof TRecord = keyof TRecord> = K extends (TRecord[K] extends TMatch ? K : never) ? K : never;

// after
type MatchingKeys<TRecord, TMatch> =
  type K = keyof TRecord,
  K extends (TRecord[K] extends TMatch ? K : never) ? K : never;

@rbuckton
Copy link
Member

rbuckton commented Oct 12, 2021

You can almost do this with conditional types and infer if you have a helper type to help with inference, but its a lot of extra boilerplate (and keeping track of extra trailing :nevers is a chore):

type Is<T extends U, U> = T;
type MatchingKeys<TRecord, TMatch> =
  [keyof TRecord] extends [Is<infer K, keyof TRecord>] ?
    K extends (TRecord[K] extends TMatch ? K : never) ? K : never 
    : never;

Unfortunately, the Is is necessary because we won't infer a base constraint for K when inferring from a tuple currently...

@weswigham
Copy link
Member Author

So, with the syntax I was thinking, the example in the OP might read:

Yeah, I can imagine allowing that. I'd want the comma expression to be optional, so you don't have to repeat yourself in cases where it's identical like that, but it does seem useful, especially alongside the multi-let form (which would already be comma-separated).

@rbuckton
Copy link
Member

Without HKTs though, this would be weird:

interface Container {
  invoke: type TInvoke<U> = () => TInvoke<U>;
}

I supposed you'd be forced to instantiate it?

interface Container {
  invoke: (type TInvoke<U> = () => TInvoke<U>)<unknown>;
}

Requiring you to specify a non-type-alias avoids that (even if its repetitive).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants