Skip to content

Enable extraction of type parameters.Β #49112

Closed

Description

@lillallol

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.

Activity

  1. fatcerberus commented on May 14, 2022

    @fatcerberus

    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.

  2. lillallol commented on May 15, 2022

    @lillallol
    Author

    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.

  3. fatcerberus commented on May 15, 2022

    @fatcerberus

    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.

  4. Retsam commented on May 15, 2022

    @Retsam
    Contributor

    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)

  5. lillallol commented on May 15, 2022

    @lillallol
    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.

    Bruce Pascoe (@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.

  6. Retsam commented on May 15, 2022

    @Retsam
    Contributor

    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.

  7. lillallol commented on May 15, 2022

    @lillallol
    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.

  8. Retsam commented on May 15, 2022

    @Retsam
    Contributor

    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;
    }
  9. lillallol commented on May 15, 2022

    @lillallol
    Author

    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.

  10. RyanCavanaugh commented on May 16, 2022

    @RyanCavanaugh
    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;

  11. lillallol commented on May 16, 2022

    @lillallol
    Author

    Ryan Cavanaugh (@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.

  12. fatcerberus commented on May 16, 2022

    @fatcerberus
    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?

  13. RyanCavanaugh commented on May 16, 2022

    @RyanCavanaugh
    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.

  14. lillallol commented on May 17, 2022

    @lillallol
    Author

    Ryan Cavanaugh (@RyanCavanaugh)

    So we are dealing with a design limitation?

    Bruce Pascoe (@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).

  15. lillallol commented on May 18, 2022

    @lillallol
    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.

  16. RyanCavanaugh commented on May 18, 2022

    @RyanCavanaugh
    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.

  17. karlhorky commented on Jun 7, 2023

    @karlhorky
    Contributor

    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);
    }
  18. karlhorky commented on Jun 7, 2023

    @karlhorky
    Contributor

    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
  19. lillallol commented on Jul 1, 2023

    @lillallol
    Author

    Ryan Cavanaugh (@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

Metadata

Metadata

Assignees

No one assigned

    Labels

    DeclinedThe issue was declined as something which matches the TypeScript visionSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions