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

JSDoc @augments doesn’t allow function calls to augment existing types #26675

Open
dotnetCarpenter opened this issue Aug 25, 2018 · 7 comments
Labels
Domain: JavaScript The issue relates to JavaScript specifically Domain: JSDoc Relates to JSDoc parsing and type generation Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Milestone

Comments

@dotnetCarpenter
Copy link

I wrote a simple extension of jest.Matchers but I can not get the typescript type checker to recognize my extension.

TypeScript Version: 3.0.1 (and 3.1.0-dev.20180825)
The description below is for 3.0.1 - for 3.1.0-dev.20180825 all jest functions show errors, so I don't even think that the parser gets to my type at all.

[ts] Cannot find name 'describe'.
[ts] Cannot find name 'test'.
[ts] Cannot find name 'expect

Used via vscode version: 1.26.1

Search Terms:
is:issue is:open jsdoc extends
is:issue is:open jsdoc @Augments
is:issue is:open jsdoc "@Augments"

I first asked this as a question on StackOverflow but after waiting a few days, I now suspect it is a bug in typescript's JSDoc implementation.

I also read the FAQ and found the link to jest but didn't find any information there either.

I'm using plain JavaScript and the code runs perfectly.

Code

// @ts-check

const getFunctorValue = F => {
  let x
  F.fmap(v => x = v)
  return x
}


expect.extend({
  /**
  * @extends jest.Matchers
  * @param {*} actual The functor you want to test.
  * @param {*} expected The functor you expect.
  */
  functorToBe(actual, expected) {
    const actualValue = getFunctorValue(actual)
    const expectedValue = getFunctorValue(expected)
    const pass = Object.is(actualValue, expectedValue)
    return {
      pass,
      message () {
        return `expected ${actualValue} of ${actual} to ${pass ? '' : 'not'} be ${expectedValue} of ${expected}`
      }
    }
  }
})

/**
* @param {*} v Any value
*/
function just (v) {
  return {
    fmap: f => just(f(v))
  }
}

describe('Functor Law', () => {
  test('equational reasoning (identity)', () => {
    expect(just(1)).functorToBe(just(1))
  })
})

Expected behavior:
expect().functorToBe should be recognized as a function.

Actual behavior:
But in the line with expect(just(1)).functorToBe(just(1)),
I get a red underline under functorToBe and the following error message:

[ts] Property 'functorToBe' does not exist on type 'Matchers<{ [x: string]: any; fmap: (f: any) => any; }>'.
any

I got jest.Matchers from writing expect() in vscode and looked at the description.

image of vscode intellisense displaying jest.Matchers as return type for expect()

Playground Link:

Related Issues:
No.

@dotnetCarpenter
Copy link
Author

@dotnetCarpenter
Copy link
Author

dotnetCarpenter commented Aug 25, 2018

After looking at the parser, it dawned on me that maybe the jest interfaces are wrongly weirdly typed.

expect.extend() takes a jest.ExpectExtendMap, which would mean that typescript should map my dynamic method to jest.ExpectExtendMap to jest.Matchers<R>, which of course is not possible.
But when I use @extends, I am telling typescript that I am extending jest.Matchers but what if I'm not?

What if I'm passing a type to the generic type jest.Matchers<R>?

jest.Matchers<functorMatcher>:

/**
 * @class
 */
const functorMatcher = {
 /**
  * @method
  * @param {*} actual The functor you want to test.
  * @param {*} expected The functor you expect.
  */
  functorToBe(actual, expected) {
    const actualValue = getFunctorValue(actual)
    const expectedValue = getFunctorValue(expected)
    const pass = Object.is(actualValue, expectedValue)
    return {
      pass,
      message () {
        return `expected ${actualValue} of ${actual} to ${pass ? '' : 'not'} be ${expectedValue} of ${expected}`
      }
    }
  }
}

expect.extend(functorMatcher)

But that doesn't work because what happens is that expect(just('foo')) returns a type jest.Matchers<just> and not a jest.Matchers<functorMatcher>

Or maybe I'm guessing too much and should just wait for someone, who really knows what's going on, to enlighten me.

@sandersn sandersn added Salsa Domain: JSDoc Relates to JSDoc parsing and type generation Suggestion An idea for TypeScript labels Aug 27, 2018
@sandersn
Copy link
Member

Currently @augments doesn't work for constructor functions, but that's not what going on here, I think. I think you want to augment the type of extends. Typically Typescript has used merging for this in the past. In other words:

interface Expect {
   // normal stuff in expect
}
declare const expect: Expect

// Filename: mine.js
interface Expect {
  functorToBe(...): { pass, message }
}

I'm not sure of the best way to express this in jsdoc. Maybe @augments is it, maybe it's not. Thoughts?

@RyanCavanaugh RyanCavanaugh added the Experience Enhancement Noncontroversial enhancements label Aug 28, 2018
@sandersn
Copy link
Member

After a bit more thought, I think the core problem is that the statement that changes the type of expect is a side-effecting function call, not a declaration. Typescript doesn’t have the ability to mutate types like that.

I think your best bet in the near term is to use a separate .d.ts file to express the type mutation as a type merge as I outlined above, although that may require some work on jest’s types if they are not amenable to merging already.

This not a satisfying solution for projects that want to be pure Javascript, however. Let’s keep this issue open to track the idea of a mutation-as-merge jsdoc tag.

@sandersn sandersn changed the title JSDoc @augments has no effect for functions JSDoc @augments doesn’t allow function calls to augment existing types Aug 29, 2018
@dotnetCarpenter
Copy link
Author

Using vscode I can find a index.d.ts file (not sure where it originates from) that has a interface to Expect.

/**
 * The `expect` function is used every time you want to test a value.
 * You will rarely call `expect` by itself.
 */
interface Expect {
    /**
     * The `expect` function is used every time you want to test a value.
     * You will rarely call `expect` by itself.
     *
     * @param actual The value to apply matchers against.
     */
    <T = any>(actual: T): Matchers<T>;
    /**
     * You can use `expect.extend` to add your own matchers to Jest.
     */
    extend(obj: ExpectExtendMap): void;
    // etc.
}

I would then add my own index.d.ts file with the following?

interface Expect {
  functorToBe(...): { pass, message }
}
// or
interface Functor {
  (value?: any): {
    fmap: (f: function): Functor
  }
}
interface Matchers<R> {
  functorToBe(actual: Functor, expected: Functor): { pass: boolean, message: string }
}

Or I would define it in my test file?

Also not sure how to describe it without resorting to the type function which does not seem to exist.

@sandersn
Copy link
Member

Take a look at Module Augmentation on https://www.typescriptlang.org/docs/handbook/declaration-merging.html. Your second idea of merging with Matchers is basically right, I think, since that's what is returned by expect. Your index.d.ts would have to reference jest's index.d.ts via the name your import it as -- probably just 'jest'.

@dotnetCarpenter
Copy link
Author

dotnetCarpenter commented Aug 29, 2018

Hmm.. this doesn't seem quite right. But I'll take it to SO and stop polluting this issue.

import { Matchers } from 'jest'

interface Functor<T> {
  (value?: any): {
    fmap: (f: value) => Functor<T>
  }
}

interface Matchers<R> {
  functorToBe(actual: Functor<T>, expected:  Functor<T>): R
}

https://stackoverflow.com/questions/52082800/how-to-describe-the-interface-of-a-simple-just-functor-in-typescript

@weswigham weswigham removed the Suggestion An idea for TypeScript label Nov 6, 2018
@weswigham weswigham added Domain: JavaScript The issue relates to JavaScript specifically and removed Domain: JavaScript The issue relates to JavaScript specifically Salsa labels Nov 29, 2018
@RyanCavanaugh RyanCavanaugh added the Suggestion An idea for TypeScript label Mar 7, 2019
@RyanCavanaugh RyanCavanaugh added this to the Backlog milestone Aug 16, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Domain: JavaScript The issue relates to JavaScript specifically Domain: JSDoc Relates to JSDoc parsing and type generation Experience Enhancement Noncontroversial enhancements Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

4 participants