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

Dynamically named tuples #44939

Open
5 tasks done
k-tten opened this issue Jul 8, 2021 · 7 comments
Open
5 tasks done

Dynamically named tuples #44939

k-tten opened this issue Jul 8, 2021 · 7 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@k-tten
Copy link

k-tten commented Jul 8, 2021

Suggestion

TypeScript's type system is extremely expansive and powerful, but one thing that I cannot seem to do is give tuples dynamic names. Eg:

type Test00 = [["name"]: number];

Or even:

type Test01 = [`name`: number];

Just like objects, we should be able to use bracket syntax to indicate that the expression inside should be used as the name.

We can then proceed to use other types to "calculate" the names.

🔍 Search Terms

  • tuple names
  • dynamic tuple names
  • changing tuple names
  • editing tuple names
  • calculated tuple names

✅ Viability Checklist

My suggestion meets these guidelines:

  • 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.

I believe this one meets the last guideline because of goals 2 and 4.

⭐ Suggestion

Dynamically/calculated/inserted tuple names instead of static ones.

📃 Motivating Example

Consider the following example (ripped from a project of mine):

type Components = -1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26;

type Decrement<X extends number> = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25][X];

type CreateArray<T, L extends number> = L extends -1 ? [] : [T, ...CreateArray<T, Decrement<L>>];

type CanonicalAxisNames = ["", "x", "y", "z", "w", "v", "u", "t", "s", "r", "q", "p", "o", "n", "m", "l", "k", "j", "i", "h", "g", "f", "e", "d", "c", "b", "a"];

type GetCanonicalAxisNames<C extends Components, R extends string[] = []> = C extends 0 | -1 ? R : GetCanonicalAxisNames<Decrement<C>, [CanonicalAxisNames[C], ...R]>;

type CreateVector<C extends Components> = {
    new (): VectorStruct<C>;
};

type VectorStruct<C extends Components> = {
    add(vector: VectorStruct<C>): VectorStruct<C>;
    add(...components: CreateArray<number, Decrement<C>>): VectorStruct<C>;
    add(scalar: number): VectorStruct<C>;
} & {
    [N in GetCanonicalAxisNames<C>[number]]: number;
}

declare function createVector<C extends Exclude<Components, -1 | 0>>(components: C): CreateVector<C>;

const Vector = createVector(3);

const v = new Vector();

v.add(0, 0, 0); // arguments are listed as components_0, components_1, and components_2

Instead of boring, bland names, I could use this new feature to give them more descriptive/detailed names.
Names are important because they describe the object in as few words as possible.
components_0 simply does not convey the same idea as x.

Suppose I create a type InsertTupleNames to create a tuple whose names are calculated, then I could use:

add(...components: InsertTupleNames<CreateArray<number, Decrement<C>>>): VectorStruct<C>;

Which means, the parameters are now named the correct names, and they make more sense to the end user.

Currently, I have to settle for a massive tuple containing all possible lists of parameter names, which is not ideal or maintainable.

💻 Use Cases

It would make more complex types/functions more readable/usable.

I think this example I have shown demonstrates that TypeScript's type system is extremely powerful, and so powerful that we can already create standalone types based on another one (aka CreateVector creates VectorStruct<...> and VectorStruct is a standalone type, functional, and solely created from CreateVector).

If we had this then we could make spread parameters and the outputted JavaScript easier to read with higher legibility.

@RyanCavanaugh RyanCavanaugh added Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript labels Jul 8, 2021
@tjjfvi
Copy link
Contributor

tjjfvi commented Jul 20, 2021

See also the somewhat off-topic discussion at #41594

@awerlogus
Copy link

Need this to generate state repositories. The basic version could look like

type StateRepository<T extends string, E> =
  & { [K in `set${Capitalize<T>}`]: (...args: [[T]: E]) => void }
     // Dynamically named tuples are used here ^^^
  & { [K in `get${Capitalize<T>}`]: () => E }

const capitalize = <S extends string>(string: S) => string.charAt(0).toUpperCase() + string.slice(1) as Capitalize<S>

const getStateRepository = <T extends string, E>(name: T, start: E): StateRepository<T, E> => {
  let state = start

  const cName = capitalize(name)

  return {
    [`set${cName}`]: data => { state = data },
    [`get${cName}`]: () => state,
  }
}

const counterRepository = getStateRepository('counter', 0)

counterRepository.setCounter(1)

const value = counterRepository.getCounter()

@k-tten
Copy link
Author

k-tten commented Oct 20, 2021

I think my example is too specific and not well presented. The more common problem people face is the one @awerlogus suggested: having a function whose return type is based on the parameters.
This would be great for ORMs for example. Instead of arbitrary names like id, we could have dynamically named tuples and the argument name would be userId, etc.
TL;DR: Dynamically named tuples would allow users to be more flexible and create more friendly complex types 🚀

@tmm
Copy link

tmm commented Oct 4, 2023

Commented on the design meeting issue related to this one with a motivating use-case. Linking it up here: #55511 (comment)

@emretepedev
Copy link

emretepedev commented Mar 11, 2024

Is there any update for this?

@NateFerrero
Copy link

NateFerrero commented Nov 21, 2024

I ran into this issue today attempting to dynamically generate id parameter names from a known field tuple for primary keys:

function get(...ids: ExtractPrimaryKeys<Entity>) {}

Unfortunately, I am limited to:

function get(ids_0: number, ids_1: string) {}

I want to be able to name the tuple generated in ExtractPrimaryKeys for better function argument documentation:

function get(organizationId: number, userId: string) {}

This would be epic!

My actual code looks like:

export type ExtractPrimaryKeys<T extends Entity<any, any>> = T["primaryKeyFields"] extends readonly [...infer U]
  ? [...{ [K in keyof U]: FieldTypeToTypeScriptType<T["primaryKeyFields"][K]>  }]
  : never;

But there is no way to dynamically set the generated tuple element name in this case.

One suggestion is that we could name generated tuple elements like [K in keyof U as "foo"]:

export type ExtractPrimaryKeys<T extends Entity<any, any>> = T["primaryKeyFields"] extends readonly [...infer U]
  ? [...{ [K in keyof U as T["primaryKeyFields"][K]["name"]]: FieldTypeToTypeScriptType<T["primaryKeyFields"][K]>  }]
  : never;

@IntusFacultas
Copy link

I ran into a similar use case, namely for some GraphQL work I'm doing.

I have interfaces auto-generated for me for resolver functions in the form

export abstract class IQuery {
   abstract myResolver(arg1: Nullable<string>, arg2: Nullable<number>): Nullable<MyResult>;
}

And when I'm writing integration tests against it, I have a utility method in the format of:

cy.graphQLRequest<TExpectedResult = unknown, TVariables = Record<string, string | number | boolean | null>>({
  operation: DocumentNode;
  variables: TVariables;
}): Chainable<TExpectedResult>;

I can get the parameters of the resolver fairly trivially through Parameters<typeof IQuery['myResolver']> but ideally I'd want it as an object, not as an array of tuples. Something like

type ParametersAsRecord<T extends (...args: any) => any> = T extends (...args: infer [ParameterName, ParameterType]) => any ? {
  [key in ParameterName]: ParameterType
} : never

would be ideal.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants