microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
101.07k stars 12.49k forks source link

Generic types from JSDoc aren't really generic #26883

Open sandersn opened 6 years ago

sandersn commented 6 years ago
/**
 * @constructor
 * @template {string} K
 * @template V
 */
function Multimap() {
    /** @type {Object<string, V>} TODO: Remove the prototype from the fresh object */
    this._map = {};
};

var Ns = {}
/** @type {Multimap<"a" | "b", number>} */
const map = new Multimap();
const n = map._map['hi']

Expected behavior: n: number

Actual behavior: n: any in 3.0; n: V in 3.1-dev.

Types resolved from functions are never properly generic, even that function has @template-specified type parameters; they're only special-cased in a few places to produce a specific instantiation of a type. They should use the normal generic type machinery that Typescript does.

michaelolof commented 5 years ago

Hello. I'm hope this is the right place for this issue. I tried opening a new bug issue but got discouraged.

My goal is to use JavaScript along with .d.ts declaration files.

So my workflow looks a little like this.

This is what my declaration file looks like.

// test.d.ts
export declare function addToCollection<T>( collection:T[], val:T):T

export as namespace test;

Then I implement it in a JS file like this.

// test.js
/** 
 * @type { test.addToCollection }
 **/
export function addToCollection( collection, val ) {
   return collection.push( val )
}

The problem here is that Generics are ignored. If the addToCollection function declaration were not generic, it would work. But a generic declaration would be ignored.

Any suggestions on this, or am I using it wrong?

sandersn commented 5 years ago

@michaelolof You are using it wrong. test.d.ts applies for users of test.js, not test.js itself. The types of test.js should come from JSDoc in its source, or from packages that it imports.

For your example, you should change the JSDoc for addToCollection

/**
 * @template T
 * @param {T[]} collection
 * @param {T} val
 * @returns {T}
 */

and you should no longer need test.d.ts.

michaelolof commented 5 years ago

Thank you for replying.

I kind of understand where you're coming from and I can probably tell this pattern is definitely not conventional.

My issue with the JSDoc approach, is that it can get really verbose. The above example is a very simple use case for generics.

Consider a typed pipe function like this in a typescript file.

export function pipe<T1, R>(
  fn1: (arg1: T1) => R,
  ...fns: Array<(a: R) => R>
): (arg1: T1) => R;

export function pipe<T1, T2, R>(
  fn1: (arg1: T1, arg2: T2) => R,
  ...fns: Array<(a: R) => R>
): (arg1: T1, arg2: T2) => R;

export function pipe<T1, T2, T3, R>(
  fn1: (arg1: T1, arg2: T2, arg3: T3) => R,
  ...fns: Array<(a: R) => R>
): (arg1: T1, arg2: T2, arg3: T3) => R;

export function pipe<T1, T2, T3, T4, R>(
  fn1: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => R,
  ...fns: Array<(a: R) => R>
): (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => R;

export function pipe<R>(
  fn1: (...args: any[]) => R,
  ...fns: Array<(a: R) => R>
): (a: R) => R {
  return fns.reduce((prevFn, nextFn) => value => nextFn(prevFn(value)), fn1);
}

I shudder to think of how this would be implemented with JSDoc. Even the TypeScript approach is still a bit to noisy. I'm in an environment where my team is still warming up to TypeScript and I'm scared seeing something like this could easily put them off.

For me i believe the biggest benefit of this .d.ts approach is hiding away all the noise that comes with complicated typings like this. There's also the advantage of seamlessly sharing interfaces extending interfaces etc. without JSDocing your entire codebase.

Also you have to consider this is an approach that works already in TypeScript. It just breaks when using generics.

I hope you do consider it.

Great job with all the work @typescript.

weswigham commented 5 years ago

I shudder to think of how this would be implemented with JSDoc.


/** @typedef {{
*  <T1, R>(
*   fn1: (arg1: T1) => R,
*   ...fns: Array<(a: R) => R>
* ): (arg1: T1) => R;
* <T1, T2, R>(
*   fn1: (arg1: T1, arg2: T2) => R,
*   ...fns: Array<(a: R) => R>
* ): (arg1: T1, arg2: T2) => R;
* <T1, T2, T3, R>(
*   fn1: (arg1: T1, arg2: T2, arg3: T3) => R,
*   ...fns: Array<(a: R) => R>
* ): (arg1: T1, arg2: T2, arg3: T3) => R;
* <T1, T2, T3, T4, R>(
*   fn1: (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => R,
*   ...fns: Array<(a: R) => R>
* ): (arg1: T1, arg2: T2, arg3: T3, arg4: T4) => R;
* }} PipeFunction
*/

/* @type {PipeFunction} / const pipe = (fn1, ...fns) => { return fns.reduce((prevFn, nextFn) => value => nextFn(prevFn(value)), fn1); };

michaelolof commented 5 years ago

Yes I've tried this. It's verbose but still suffers from the same problem as the .d.ts approach.

  1. The arrow functions are necessary, typings in normal functions would just get ignored.
  2. The typings in the arrow function context are also ignored. Parameters fn1 and fns are of type any. It is the same problem I faced using declaration files.

For a simple one line implementation like the as above this might not be a problem, but implementing a generic function with about 7+ lines of code and you start to see how this might be a problem.

trusktr commented 4 years ago

This is where the internet led me regarding how to call functions with specific generic args using JSDoc.

How would we call, for example, the above PipeFunction with specific generic args instead of inferred ones? Or does JSDoc not support explicit generic args for function calls?

@sandersn If I understand correctly, that's what you're saying we can't currently do, right?

It would be really great to have all the features of TS in JSDoc, because then it means we could use generic JSDoc tooling (not just the too-inflexible TSDoc) without duplicating type information in both source and comments.

sandersn commented 4 years ago

@trusktr You are correct, there is no way to call functions with type arguments. We view it as a code smell in TS, since it's basically a more-widely-propagated, but non-obvious, cast. I recommend just using a cast in JS, and later rewriting the function so that its type arguments can be inferred [1].

We do not have general plans to replicate all of TS's features in JS, since we think that most people who want TS' features will eventually switch to TS anyway. The main user we care about for JSDoc is the one who never thinks about TS, or types for that matter.

[1] This may mean getting rid of all type parameters and forcing callers to cast.

danielo515 commented 3 years ago

I have a much simpler scenario that you may fall between something as complex as the previous example and very basic types. I think many people may be interested into being able to use generics like this on JSDoc:

/**
 * @typedef { {response: any, error?: {status: number}} } apiResponse
 */

/**
 * Injects the user namespace on database Reference to the provided handler
 * @template T
 * @param {(db: firebase.database.Reference, args: T) => Promise<apiResponse>} handler
 * @returns { (args:T) => Promise<apiResponse> }
 */

The problem is that any function that I wrap gets the generic type as any:

Screenshot 2021-02-27 at 11 17 03

I can "force" the type by doing this:

/**
 * @type { (args: {name: string}) => Promise<apiResponse> }
 */
export const startSession = withDb((db, { name }) => {
});

Is there any shorter way? I just don't want to have the return type on every wrapped function

jakobrosenberg commented 3 years ago

Is there any work around for creating an object signature with a generic value?

btakita commented 2 years ago

I'm looking for using generics in types defined in .d.ts files from .js using jsdoc. I'd like to be able to not have to use the tsc but have the benefits of a rich type system with .js files. jsdoc types are simply not as powerful as typescript types & syntax is often not as nice as typescript types. It would be great to have the source code defined in js while referencing types in d.ts.

danielo515 commented 2 years ago

I'm looking for using generics in types defined in .d.ts files from .js using jsdoc. I'd like to be able to not have to use the tsc but have the benefits of a rich type system with .js files. jsdoc types are simply not as powerful as typescript types & syntax is often not as nice as typescript types. It would be great to have the source code defined in js while referencing types in d.ts.

That is already possible. You can import them reference types from jsdoc comments

btakita commented 2 years ago

That is already possible. You can import them reference types from jsdoc comments

It kindof works. I was not able to get generics to work correctly from jsdocs though. It would be great if there was a guide on using generics from jsdocs.

https://github.com/microsoft/TypeScript/issues/27387

LAC-Tech commented 2 years ago

We do not have general plans to replicate all of TS's features in JS, since we think that most people who want TS' features will eventually switch to TS anyway. The main user we care about for JSDoc is the one who never thinks about TS, or types for that matter.

I get that you have to prioritise, but it's sad to hear.

I strongly prefer to write Javascript instead of Typescript just because it removes a building step. But I still "think about types".

For me it's about setting up simpler projects with less moving parts.

btakita commented 2 years ago

I strongly prefer to write Javascript instead of Typescript just because it removes a building step. But I still "think about types".

There is a growing culture of library developers who write the implementation javascript & hand-write .d.ts. I've seen a few prominent developers who write js for their library code & ts for their app code.

Writing in js removes the build step, which helps with tooling compatibility (e.g. vite) & build performance. With the new tooling projects coming out, it helps to keep libraries simple.

It would be great if there was a guide on using generics from jsdocs.

https://docs.joshuatz.com/cheatsheets/js/jsdoc has been helpful