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

Add named type arguments #38913

Open
5 tasks done
pedrolcn opened this issue Jun 3, 2020 · 8 comments
Open
5 tasks done

Add named type arguments #38913

pedrolcn opened this issue Jun 3, 2020 · 8 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

@pedrolcn
Copy link

pedrolcn commented Jun 3, 2020

Search Terms

generics, type parameters, named parameter, named type parameter, type argument, named type argument

Suggestion

It should be possible to pass type arguments to a generic by name rather than positionally, eg.

interface Foo<T = SomeDefaultType, U> { ... }

// Current syntax
const foo: Foo<SomeDefaultType, string> ...

// Proposed syntax
const foo: Foo<U = string> ...
// yields foo: Foo<SomeDefaultValue, string>

This is loosely inspired on python's named arguments:

def foo(bar = "I'm bar", baz):
    ...

foo(baz="I'm baz")

Use Cases

Generics only accept positional type arguments. If you have a generic accepting many type arguments, most or all of which having default values such as:

interface Handler<Type = string, TPayload = object, TOutput = void> {
  type: Type
  handle(payload: TPayload): TOutput
}

Let's say we have a class which implements the Handler interface but the defaults for Type and TPayload are fine for us and we only want to specify a type for TOuput, currently it is mandatory that we pass type arguments for Type and TPayload:

class Foo implements Handler<string, object, Promise<number>>

If it was possible to pass type arguments by name we could use the considerably terser form:

class Foo implements Handler<TOutput=Promise<number>>

Examples

Fastify exposes types generic over many parameters with default values, such as

  interface FastifyRequest<
    HttpRequest = http.IncomingMessage,
    Query = DefaultQuery,
    Params = DefaultParams,
    Headers = DefaultHeaders,
    Body = DefaultBody
  > { ...

With the proposed syntax we could create specialized interfaces with much less overhead such as

import * as fastify from 'fastify';

const app = fastify();
app.get('/users/:id', async (req: FastifyRequest<Params = {id: string }>, res: FastifyReply) => {
   // req.params is strongly typed now
})

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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.
@RyanCavanaugh RyanCavanaugh changed the title Allow named type parameters on generics Add named type arguments Jun 3, 2020
@RyanCavanaugh
Copy link
Member

The arguments are the thing that go in at the use site; the parameters are the ones at the declaration site.

@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 Jun 3, 2020
@pedrolcn
Copy link
Author

pedrolcn commented Jun 3, 2020

The arguments are the thing that go in at the use site; the parameters are the ones at the declaration site.

Thanks for the clarification, I always mix these terms up. I updated the description to use them correctly

@Airblader
Copy link

This is a duplicate of #23696 and has been superseded by #26349 since it doesn't suffer from issues such as this one.

@ivan-kleshnin
Copy link

ivan-kleshnin commented Feb 24, 2021

@Airblader how a weaker proposal can supersede a stronger one?

(optional sequential type arguments) vs (named type arguments). I don't buy that issues in #26346 are inherent to the idea itself.

@Airblader
Copy link

Airblader commented Feb 24, 2021

You can call it an alternate approach to the raised use-cases instead of a superseding approach, if you want, but that's what @weswigham called it when he closed the issue. Either way, this is a duplicate.

The use-case raised in the issue here can just as well be solved with the superseding/alternate approach, so it's a valid replacement. In contrast, it doesn't raise issues such as type argument naming becoming public API.

@paulnelson2
Copy link

paulnelson2 commented Feb 16, 2022

@Airblader The OP's goal of making the declaration terser would indeed be covered by pr #26349, but there's another use imo, which is clarity to the reader, in the same way that named arguments to functions confer.

I'd rather say this:

(1)

interface A extends B<Indexer=string, Counter=number, Timestamper=Date> {}

than this:

(2)

interface A extends B<string, number, Date> {}

In 2, does the reader know what string is for in B? Or number or Date?
(contrived example)

@owenallenaz
Copy link

@paulnelson2 In addition to the example presented there, there is also the case where the user wants to utilize one of the arguments, but not all. For example in express there is following interface:

interface Request<
        P = core.ParamsDictionary,
        ResBody = any,
        ReqBody = any,
        ReqQuery = core.Query,
        Locals extends Record<string, any> = Record<string, any>
    > extends core.Request<P, ResBody, ReqBody, ReqQuery, Locals> {}

It is quite logical to want to overwrite just one of those params, such as the ReqBody, in which I would like to be able to do:

app.get("/", (req: Request<ReqBody = { foo: string, bar: boolean }>, res) => {
  // it knows it's a string and must exist
  const foo = req.body.foo;
});

@Zamiell
Copy link
Contributor

Zamiell commented Jul 10, 2022

In the OP, pedrolcn proposes that named type arguments would allow end-users to more-easily instantiate generic objects with lots of default parameters.

However, I believe that named type arguments have an entirely different utility - for documentation. Consider the following.

Before TypeScript 4.0, the following code was common:

/** The first element is the index of the foo array, the second element is the index of the bar array. */
type MyTuple = [number, number];

This is not ideal, because we are forced to write comments to explain what the code does. TypeScript 4.0 fixes this problem by allowing us to write more expressive code with the named tuple feature:

type MyTuple = [fooArrayIndex: number, barArrayIndex: number];

Much better! No comments necessary.

Next, let's consider the case of a Map:

/** The map keys are foo array indexes, the map values are bar array indexes. */
const myMap = new Map<number, number>();

The similarity to the previous example should be clear. It would be ideal to refactor away this JSDoc comment in the exact same way that we just did with the tuple. But how? There's no analogous "named tuple" feature for generic type parameters.

What we really want to do is to write this, using the same colon-syntax as with named tuples:

const myMap = new Map<fooArrayIndex: number, barArrayIndex: number>();

Or, using the equals-syntax that pedrolcn proposes in the OP:

const myMap = new Map<fooArrayIndex = number, barArrayIndex = number>();

Either way, the idea is that the aliases should show up whenever someone mouses over the map in VSCode, making the code self-documenting.

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

No branches or pull requests

7 participants