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

Enable extraction of type parameters. #49112

Closed
5 tasks done
lillallol opened this issue May 14, 2022 · 19 comments
Closed
5 tasks done

Enable extraction of type parameters. #49112

lillallol opened this issue May 14, 2022 · 19 comments
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript

Comments

@lillallol
Copy link

lillallol commented May 14, 2022

Suggestion

πŸ” Search Terms

allowJs, checkJs, no compile, JSDoc type import, type parameter extraction

βœ… 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.

⭐ Suggestion

Enable extraction of type parameters with a utility function called TypeParameters.

  • ./privateApi.ts
    export type IChunk = <T>(array : T[],length : number) => T[][];
    export type IChunkTypeParameter = TypeParameters<IChunk>[0];// this is actually T

πŸ“ƒ Motivating Example and πŸ’» Use Cases

  • ./chunk.js
    /**@type {import("./privateApi").IChunk}*/
    const chunk = (array,length) => {
        /**
         * @type {(typeof array)[]}
         * I do not like having logic inside the type JSDoc tag. 
         * It can easily get ugly for more complicated cases.
         * I want to be able to import the type for `toReturn`.
         * For that I need `TypeParameters` to be enabled.
         */
        const toReturn = [];
        // some code that calculates `toReturn`
        return toReturn;
    }

Question

I would like to know whether the design of TypeScript is such, that TypeParameters is possible.

@fatcerberus
Copy link

fatcerberus commented May 14, 2022

I don’t understand what IChunkTypeParameter would even be. T is an unbound type parameter in this context, there is no type assigned to it. It’s like asking what the value of a function parameter is before you call the function, it’s nonsensical.

@lillallol
Copy link
Author

lillallol commented May 15, 2022

It’s like asking what the value of a function parameter is before you call the function, it’s nonsensical.

Well I made my case already why it makes sense. Also you are not asking what the value is. You are just assigning a label to it.

@fatcerberus
Copy link

The β€œvalue” in this case is the type assigned to T, so no, it doesn’t make sense - what do you expect IChunkTypeParameter to even be? It can’t be a type since the generic hasn’t been instantiated yet, so you’re going to have to give an example of how this would be used.

@Retsam
Copy link

Retsam commented May 15, 2022

@lillallol Pretty sure this isn't possible, at least not as you're suggesting. The big issue is that IChunkTypeParameter only has meaning contextually - it only makes even potentially sense inside of an IChunk annotated function. But types can't be contextual like that.

What would it mean if someone wrote code like:

// MyModule.ts
import { IChunkTypeParameter } from "lib";

// What type is x here?
const x: IChunkTypeParameter = "is this allowed?";

Generally, I'd recommend avoiding the "type alias for a generic function" pattern in favor of generic functions, where possible. Instead of code like:

const chunk: IChunk = (array, length) => {
    const toReturn: Array<typeof array> = [];
    return toReturn;
}

I'd usually recommend:

function chunk<T>(array: T[], length: number) => {
    const toReturn: T[][] = [];
    
    return toReturn;
}

There's some cases where extracting the whole type of the function out into its own type can be useful, but I think they're somewhat rare.

(I used TS syntax throughout, because I'm way more comfortable with it - I don't think any of this problem is really unique to JS + JSDoc types syntax)

@lillallol
Copy link
Author

@Retsam

it only makes even potentially sense inside of an IChunk annotated function.

Yes it makes sense only inside a chunk annotated function.

What would it mean if someone wrote code like:

// MyModule.ts
import { IChunkTypeParameter } from "lib";

// What type is x here?
const x: IChunkTypeParameter = "is this allowed?"

Of course this is not allowed since it is not possible to know whether T is of type string. Generics should be treated as read only anyway. You can of course do something like this:

type IChunkToReturn = IChunkTypeParameter[][];

Generally, I'd recommend avoiding the "type alias for a generic function" pattern in favor of generic functions, where possible.

Why?

I don't think any of this problem is really unique to JS + JSDoc types syntax

I do not understand this sentence.

@fatcerberus

what do you expect IChunkTypeParameter to even be?

I already answered that. IChunkTypeParameter is the first type parameter of IChunk.

so you’re going to have to give an example of how this would be used.

I already did. Provide extra cases like @Retsam and I would gladly answer.

@Retsam
Copy link

Retsam commented May 15, 2022

@lillallol

Yes it makes sense only inside a chunk annotated function.

I think this makes this suggestion pretty much a non-starter. I'm pretty sure there aren't any examples of a type that works like this: having the type alias's type be implicitly contextual to where it's used is rather strange, so yes, I'd say it's probably close to "impossible".

Certainly, I suspect it would take a far more compelling reason than "it's slightly more convenient in this fairly niche case".


Generally, I'd recommend avoiding the "type alias for a generic function" pattern in favor of generic functions, where possible.

Why?

Mostly I avoid annotating whole function types because it's a lot more convenient, less repetitive:

const identity: <T>(x: T) => T = (x) => x;
// a lot more 'noisy' than the equivalent:
const identity = <T>(x: T) => x;

But also in part because of stuff like this. You just don't need something like TypeParameters if the generic parameter isn't 'hidden' in a non-generic type alias.

@lillallol
Copy link
Author

Mostly I avoid annotating whole function types because it's a lot more convenient, less repetitive:

This can happen only when you write both concretions and types in .ts files. When you are writing types in .ts files and import them via JSDoc imports in .js files (because you want to avoid compiling) you can not do that (you actually can access the T but it is ugly and painful). You are also mixing implementation with intend, which I generally try to avoid because of many reasons.

Certainly, I suspect it would take a far more compelling reason than "it's slightly more convenient in this fairly niche case".

Look I can understand that using TypeScript without the need to compile is something that is not widely adopted, and maybe TypeScript maintainers will not give high priority for such features. I would like to have a clear answer from the TypeScript maintainers though, on whether the feature is intrinsically impossible due to TypeScript design.

@Retsam
Copy link

Retsam commented May 15, 2022

@lillallol You can still write the equivalent code in JS files, though:

/**
 * @template T
 * @param {T[]} items
 * @param {number} length
 */
function chunk(items, length) {
    /** @type {T[][]} */
    const toReturn = [];

    return toReturn;
}

@lillallol
Copy link
Author

lillallol commented May 15, 2022

You can still write the equivalent code in JS files, though

Yes you are right. Here are my objections though:

  1. JSDoc can get ugly and verbose for more involved cases
  2. There is no separation of intend and implementation

I do consider the benefits that occur from these two reasons (especially the second one) enough for this feature to be adopted.

@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Declined The issue was declined as something which matches the TypeScript vision labels May 16, 2022
@RyanCavanaugh
Copy link
Member

This is conceptually unmoored in a number of ways. This isn't even about TypeScript; AFAIK any language with type parameters does not enable this sort of operation.

  • An unresolved type parameter appearing outside a generic context is meaningless, especially one attached to a function type
  • Asking for "the type parameters" of something is also meaningless in a structural type system. Once a generic type has been instantiated, you generally can't "go back" in a way that doesn't introduce assymmetries

In cases where this would make sense you can use something like type GetFoo<T> = T extends { foo: infer U } ? U : never;

@lillallol
Copy link
Author

lillallol commented May 16, 2022

@RyanCavanaugh

An unresolved type parameter appearing outside a generic context is meaningless
Asking for "the type parameters" of something is also meaningless in a structural type system.

I explained how it is not meaningless based on the initial example I provided but also the conversation I had in the previous comments. Is that not enough?

Also the type parameter is virtually outside a generic context. All I am asking is that the context that enables us to use T here:

const chunk = <T>(array : T[],length : number) => {
    const toReturn : T[][] = [];
    // calculation of toReturn omitted for brevity
    return toReturn;
}

to be enabled via something like TypeParameters<typeof chunk>[0]. Of course the extracted type parameter can be used to define other types, however they would make sense only inside the definition of chunk. By the way does using typeof makes it easier for TypeScript?

Once a generic type has been instantiated, you generally can't "go back" in a way that doesn't introduce assymmetries

How does that relate to my feature request? Care to elaborate with an example? Are you referring to more involved cases?

In cases where this would make sense you can use something like type GetFoo = T extends { foo: infer U } ? U : never;

Yes in my projects, for the initial example, I did use infer array element inside a JSDoc type tag. Then I realized I can just use /**@type {array[]}*/. But the JSDoc @type tag can get really ugly for more involved cases. TypeParameters will solve that.

@fatcerberus
Copy link

fatcerberus commented May 16, 2022

const chunk = <T>(array : T[],length : number) => {
    const toReturn : T[][] = [];
    // calculation of toReturn omitted for brevity
    return toReturn;
}

This example keeps getting posted, but you haven't yet given any examples of how TypeParameters (the actual requested feature) would be used in practice. All you've brought up is the idea of using TypeParameters<typeof chunk>[0] in place of T here, but what problem does that actually solve that can't already be solved by... just making the function generic and using T?

@RyanCavanaugh
Copy link
Member

I get that you don't like any of the existing solutions, but the proposed idea here is not a viable solution because it would introduce a type that's completely meaningless outside of the context where the type should have been declared in the first place.

@lillallol
Copy link
Author

lillallol commented May 17, 2022

@RyanCavanaugh

So we are dealing with a design limitation?

@fatcerberus

but what problem does that actually solve that can't already be solved by... just making the function generic and using T?

Good question. Here is the answer:

  1. I want to use TypeScript without the need to compile, since that makes DX better.
  2. I do want to separate intend and implementation, since that makes me need no documentation generation library for my libraries, no d.ts generator, but also makes my project more maintainable. I can give an example if you want.

If you try to adhere to these two points then you will realize that you need a feature like TypeParameters (and strong contextual typing but this off topic).

@lillallol
Copy link
Author

By the way I would like to mention that for the example I have provided, there is a solution like this:

  • ./privateApi.ts
export type IChunk = <T>(
    array : T[],
    length : number,
    toReturn? : T[][],
) => T[][];
  • ./chunk.js
/**@type {import("./privateApi").IChunk}*/
export const chunk = (array,length,toReturn = []) => {
    /* some code that calculates `toReturn` */
    return toReturn;
}

Although that might not feel hacky for the context example, I have faced other examples where the extra parameter serves only the function definition and it is not supposed in any case to be provided a value by the consumer of the function. This can be solved by defining wrapper functions that hide the parameters that are not supposed to be exposed. For the example I have provided this is done like this:

  • ./privateApi.ts
export type IChunkPrivate = <T>(
    array : T[],
    length : number,
    toReturn? : T[][],
) => T[][];

export type IChunk = <T>(
    array : T[],
    length : number,
) => T[][];
  • ./chunk.js
/**@type {import("./privateApi").IChunkPrivate}*/
export const chunkPrivate = (array,length,toReturn = []) => {
    /* some code that calculates `toReturn` */
    return toReturn;
};

/**@type {import("./privateApi").IChunk}*/
export const chunk = (...parameters) => chunkPrivate(...parameters);

A feature like TypeParameters would prevent these hacks.

@RyanCavanaugh
Copy link
Member

So we are dealing with a design limitation?

No. We "could" do this (indeed, bugs have existed where you could accidently make this happen), but we're not going to, because it doesn't make any sense to allow a type parameter to escape its enclosing context, because outside of that context, there's no meaning to give those types.

@karlhorky
Copy link
Contributor

karlhorky commented Jun 7, 2023

I was looking for something similar today, but to extract the part after extends in a type parameter, to allow for creating a wrapper around a generic function:

import lib from 'lib'

export function myLib<LibTypeParam extends TypeParameters<typeof lib>[0]>(
  ...libParameters: Parameters<typeof lib>
) {
  /* ... do something custom ... */

  return lib<LibTypeParam>(...libParameters);
}

@karlhorky
Copy link
Contributor

karlhorky commented Jun 7, 2023

Workaround

The workaround that we found for replicating the wrapper pattern above was to use casting, although I'm not sure if this works in every case:

import lib from 'lib'

export (function myLib(
  ...libParameters: Parameters<typeof lib>
) {
  /* ... do something custom ... */

  return lib(...libParameters);
}) as typeof lib

@lillallol
Copy link
Author

lillallol commented Jul 1, 2023

@RyanCavanaugh By the way for the following code snippet:

const chunk = <T>(array : T[],length : number):T[][] => {
    const toReturn = [];
    // calculation of toReturn omitted for brevity
    return toReturn;
}

can a static type system (not just TypeScript) be made to infer the type of const toReturn = []; because of return toReturn, since the return type of chunk is known? That would reduce the need for extracted type parameters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Declined The issue was declined as something which matches the TypeScript vision Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

5 participants