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

Type aliasing in Generic parameters #47457

Closed
cefn opened this issue Jan 15, 2022 · 3 comments
Closed

Type aliasing in Generic parameters #47457

cefn opened this issue Jan 15, 2022 · 3 comments

Comments

@cefn
Copy link

cefn commented Jan 15, 2022

Suggestion

Aliasing complex type compositions to short names within generic declarations would help with readability and refactoring, while allowing them to depend on other Generics. For example a class declaration could define types to be used within its own block for reuse with a short name.

An existing complex generic class declaration from a project I'm working on like...

export abstract class Vault<I extends ItemSchema, L extends ListDefLookup<I>> {

...could be extended to define named types Item and Key<Name> which otherwise re-appear in full in almost every method in its definition. Below you can see a draft declaration using imaginary syntax which re-uses the as and infer keywords for this purpose...

export abstract class Vault<
    I extends ItemSchema, 
    L extends ListDefLookup<I>, 
    z.infer<I> as Item, 
    z.infer<L[infer Name]["keySchema"]> as KeyType<Name>
  >

See the fuller example below.

This would not fundamentally change the expressivity of the language, but help with e.g. changing how those named types are composed in a single place and making it easier to read code that depended on them.

🔍 Search Terms

List of keywords you searched for before creating this issue...

  • alias generic types

✅ Viability Checklist

  • [✅] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [✅] This wouldn't change the runtime behavior of existing JavaScript code
  • [✅] This could be implemented without emitting different JS based on the types of the expressions
  • [✅] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • [✅] This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

📃 Motivating Example

Allowing aliases to appear in generic definitions means complex types can be defined in one place, even if they are dependent on Generic bindings, rather than duplicated in every location they are used.

💻 Use Cases

Below is an example drawn from an interface defined against the zod library, a schema library imported to deliver both runtime and compile-time checks. The class therefore directly refers to the schema AND the types derived from the schema, and crucially there is a 'type lookup' ListDefLookup which uses a lookup of zod runtime objects as a workaround for higher-order-types being absent. No doubt similarly complex types have arisen in other use cases. The implementation works fine, but is super-hard to read.

It sometimes seems like Item extends z.infer<I> = z.infer<I> would be equivalent, but this introduces an additional 'loose' type that commonly creates compile-time errors, when the compiler correctly detects that some class might have narrowed the type by overriding the default set by = so the types are not reliably equal.

Ideally it would be possible to simplify the abstract class Vault below to alias the composition z.infer<I> as Item once, and then refer to it throughout.

Class without aliasing

export abstract class Vault<I extends ItemSchema, L extends ListDefLookup<I>> {
  constructor(readonly itemSchema: I, readonly listDefLookup: L) {}

  abstract save: (
    item: Optional<z.infer<I>, "id" | "rev">
  ) => Promise<{ id: string; rev: string }>;
  abstract load: (id: string) => Promise<z.infer<I>>;
  abstract all: (options?: {
    aboveId?: string;
    limit?: number;
  }) => AsyncGenerator<z.infer<I>>;
  abstract list: <ListName extends keyof L>(
    name: ListName,
    options?: {
      aboveKey?: z.infer<L[ListName]["keySchema"]>,
      limit?: number;
    }
  ) => AsyncGenerator<[z.infer<L[ListName]["keySchema"]>, z.infer<I>]>;
}

...and this terser form is drafted using the keyword as within the class's generic declaration....

Class using (imaginary) as aliasing syntax

export abstract class Vault<
    I extends ItemSchema, 
    L extends ListDefLookup<I>, 
    z.infer<I> as Item, 
    z.infer<L[infer Name]["keySchema"]> as KeyType<Name>
  >
{
  constructor(readonly itemSchema: I, readonly listDefLookup: L) {}

  abstract save: (
    item: Optional<Item, "id" | "rev">
  ) => Promise<{ id: string; rev: string }>;
  abstract load: (id: string) => Promise<Item>;
  abstract all: (options?: {
    aboveId?: string;
    limit?: number;
  }) => AsyncGenerator<Item>;
  abstract list: <ListName extends keyof L>(
    name: Name,
    options?: {
      aboveKey?: KeyType<ListName>;
      limit?: number;
    }
  ) => AsyncGenerator<[KeyType<ListName>, Item]>;
}

Below are the types referenced by the example in case they are needed to make sense of it.

import { z, ZodSchema } from "zod";
import { Optional } from "./types";

export type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

export interface Versioned {
  id: string;
  rev: string;
}

export type ItemSchema = ZodSchema<Versioned>;

export type ListDefLookup<I extends ItemSchema> = {
  [name: string]: ListDef<I, any>;
};

export interface ListDef<
  ItemSchema extends ZodSchema<Versioned>,
  KeySchema extends ZodSchema<any>
> {
  keySchema: KeySchema;
  getKey: (item: z.infer<ItemSchema>) => z.infer<KeySchema>;
}
@MartinJohns
Copy link
Contributor

Essentially a duplicate of #41470.

@cefn
Copy link
Author

cefn commented Jan 16, 2022

Thanks, @MartinJohns agreed the intent is the same with the syntax proposal different. If a feature was landed to allow new named types within a generic context (i.e. that could compose new named types from other types exploiting that generic context) I would use it to solve these problems whatever the syntax.

@cefn cefn closed this as completed Jan 16, 2022
@cefn
Copy link
Author

cefn commented Jan 16, 2022

Closed in favour of #41470

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

No branches or pull requests

2 participants