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

Type Definitions for TypeScript #272

Open
Lordfirespeed opened this issue Dec 1, 2023 · 20 comments
Open

Type Definitions for TypeScript #272

Lordfirespeed opened this issue Dec 1, 2023 · 20 comments

Comments

@Lordfirespeed
Copy link

No description provided.

@dmitriz
Copy link
Owner

dmitriz commented May 8, 2024

?

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Jun 22, 2024

.d.ts type declarations allow vanilla javascript .js to be consumed from TypeScript projects, enabling IDE completions and intelliSense similar to strongly typed languages.

@dmitriz
Copy link
Owner

dmitriz commented Aug 23, 2024

Can you give a specific example of a use case for this library?

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Aug 23, 2024

It's impossible to consume your library from a TypeScript project without type declarations.
Consuming your library from a TypeScript project is the use-case.

// today, typescript shows an error here: Could not find a declaration file for 'cpsfy'. 'cpsfy' implicitly has an 'any' type. (TS7016)
import { pipeline } from "cpsfy"

const foo: string = "fizz  \n  \tbuzz"

// typescript should be able to infer 'bar' is `string[]` using cpsfy type declarations
// today, it cannot
const bar = pipeline(foo)(
  line => line.split("\n"),
  words => words.map(word => word.trim())
)

@dmitriz
Copy link
Owner

dmitriz commented Aug 23, 2024

From your code, I understand you are using the pipeline operator, so you need to import it, not the CPS operator which serves a different purpose.

Your use of the pipeline is correct but bar seems to be an array, not a string.

By 'impossible to consume', do you mean Typescript is not able to handle plain JS modules without errors? What about the pipe operator from the Ramda library? You could use it in a similar way, except foo would have to be at the end, which breaks the natural order and is the reason I prefer pipeline. But otherwise, it is a similar general purpose utility (not related to the CPS functions). Does it mean the Ramda library is also impossible to consume from Typescript?

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Aug 23, 2024

bar is a string[]. An array of strings. Can also be written Array<string>.

Ramda has a DefinitelyTyped package: https://www.npmjs.com/package/@types/ramda i.e. type definitions are provided, just not directly.
So it can be safely consumed from TypeScript.

You can consume plain JS from typescript without errors. But you lose out on all the benefits of using TypeScript in the first place, because your type-safety goes out the window.

@dmitriz
Copy link
Owner

dmitriz commented Aug 23, 2024

Ok, an array of strings :)

I see these are a separate package and a repository for Ramda.
Then similar ones should be possible to create for cpsfy I presume, if anyone likes to do it, I'll be happy to include a link. :)

@Lordfirespeed
Copy link
Author

Then similar ones should be possible to create for cpsfy I presume, if anyone likes to do it, I'll be happy to include a link. :)

@types/ramda actually depends on https://www.npmjs.com/package/types-ramda, which is maintained by the ramda maintainer.

the readme states intention to move the type declarations:

... such that this repo can eventually be moved into the core ramda repo

Basically, @types packages are band-aids. It is much easier to maintain types when they are kept with the source code, ie. in this repository, in the case of cpsfy.

@dmitriz
Copy link
Owner

dmitriz commented Aug 23, 2024

My intent is to keep this repo as light and tiny as possible with zero dependency, which I see as one of the attractive features.

I would be worried about adding any complexity and especially dependency packages from npm that unfortunately tend to deteriorate over time and make installation a nightmare with lots of warnings. Even now I am getting massive numbers of warnings just from the testing packages that used to be absent in the past.

If those additions could be done separately, I would prefer it, especially since I don't use TS myself.

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Aug 23, 2024

You don't need any dependencies. Just a .d.ts file.

For checking the syntax of the file you can optionally install typescript as a development dependency. No runtime dependencies are required.

The typescript package has been maintained by Microsoft for years now. It's not going anywhere.

@dmitriz
Copy link
Owner

dmitriz commented Aug 23, 2024

The additional TS package, not the smallest one which is not really related to the project, would be a dealbreaker for me, it is against the minimalistic philosophy to keep things to a bare minimum.

Given that the same feature can be achieved in an independent repository like for Ramda, which can be maintained separately in a fully decoupled fashion, e.g. only for certain operators rather than for all, I would prefer that solution here as well.

@Lordfirespeed
Copy link
Author

Having a separate package is not minimalist, it's maximalist.

It creates more work for everyone. An API change here, requires communication to maintainers over there, who have to make a PR at https://github.com/DefinitelyTyped/DefinitelyTyped, and then consumers have to remember to bump @types/cpsfy as well as cpsfy.

'Not the smallest one' - yes, the TypeScript package is 21MB ... with no dependencies.

ava with dependencies is 18MB.
tape with dependencies is another 18MB.
documentation with dependencies is 81MB (used via npx).
markdown-toc with dependencies is 85MB (used via npx).

You don't need to install typescript at all, you can use it via npx if that's your preference:

npx --package typescript tsc [args]

'Not really related to the project' - arguably, neither are ava or tape. Yet you are happy to use those?

@dmitriz
Copy link
Owner

dmitriz commented Aug 23, 2024

At this stage, this library is mature and changes are rare. So it wouldn't be much work for me to ping the other maintainers. Or they can subscribe to changes and get notifications when new versions come out. Or even automate things via Github actions.

On the other hand, if this additional feature is kept locally, any change to the core library will add more work for me to take care of updating the TS support, which I am not too familiar with. Other contributors may be busy at the time, and so the library's quality will decline. Or else, additional tooling and automation would have to be written and maintained, adding more complexity. This is the real issue.

Plus, there would have to be some automation tools to check the quality of the types, where, as I said I have no experience.

Also, having a separate repository will give more freedom and empower other contributors not to wait for their PR approval when adding tools as they like.

Ramda had not made that anticipated change yet, and no timeline is given, despite their massive number of collaborators. I can only guess they have some serious reason like that change may not be trivial and could be a lot of work.

It will likely have to start with one or two operators that people actually use in their TS project and that matter to them. Your example contains pipeline, which is just a basic general-purpose utility, not the core method of the library and having nothing to do with CPS functions.

Would you know how to write those type declarations for the more advanced operators like map applying a variadic tuple of functions to a cps function with outputs from multiple callbacks? Any types are allowed for the outputs, so the only type check would be to execute the operator as it is I presume.

Let us look at the simple identity function f=x=>x. What kind of type declaration does it need?
Yes, you want to infer that it turns strings into strings, which is done by direct execution, with all information needed already available in the function. Isn't it how the type inference is done with TS?

As for the dependencies, indeed, with npx it would be less of a problem. Unfortunately, both ava and tape would not work with npx without installation, would they?

Otherwise, I'd be more than happy to remove them :)

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Aug 23, 2024

Unfortunately, both ava and tape would not work with npx without installation, would they?

I see no reason they wouldn't. npx documentation works by downloading the documentation package from the NPM registry and running its default executable. npx ava should work in much a similar manner, as should npx tape.

image

Would you know how to write those type declarations for the more advanced operators like map applying a variadic tuple of functions to a cps function with outputs from multiple callbacks? Any types are allowed for the outputs, so the only type check would be to execute the operator as it is I presume.

I have some experience with this, yes - it'll be a bit tricky and require use of type parameters (generics). This file contains declarations for a similar-ish API to map.

Let us look at the simple identity function f=x=>x. What kind of type declaration does it need?
Yes, you want to infer that it turns strings into strings, which is done by direct execution, with all information needed already available in the function. Isn't it how the type inference is done with TS?

In typescript, the code

const f = x => x

will implicitly type f as (x: any) => any because no parameter annotations are provided - so x is implicitly typed as any.
This is not desirable, because when we call f, the return type will be 'wide' (encompassing many possible types):

const x = "foobar"  // x has inferred type `string`
const y = f(x)  // y has inferred type `any` - it could be a number, or an object, etc. as far as typescript is concerned

We could annotate f like this:

const f = (x: string) => x

where x: string annotates that the parameter x must be assignable to type string, so typescript can infer that the return-type of f will also be string.

Or we could annotate the return-type explicitly:

const f = (x: string): string => x

To retain the information that the type returned will be in some way related to the type of the provided parameter, we would have to use a generic type parameter:

const f = <T>(x: T): T => x

the <...> is where we define our type parameters (and optionally constraints and defaults for each). In this case we have defined one named T. We can then annotate parameters / the return type to be in some way related to T.
When we call f, we can either provide T explicitly or allow typescript to infer it from usage (the parameter types):

const x = "foobar"  // x has inferred type `string`
const a = f<string>(x)   // a has inferred type `string`
const b = f(x)  // the generic parameter `T` is inferred to be `string` because `x` is `string`. b has inferred type `string`

const y = 123
const c = f(y)  // c has inferred type `number`

@dmitriz
Copy link
Owner

dmitriz commented Aug 24, 2024

The problem with AVA and tape tests is that the tests are loading those runners as modules, which doesn't seem to work with npx as far as I understand. Even after I install it with npx, the modules are still declared as missing and running tests leads to errors. Is there any way around it?

I like the declarations

const f = <T>(x: T): T => x

as they would help to understand the operators.
These have been inspired by Haskell, as most ideas from functional programming we use, which is good. But there might be some difficulties with variadic functions that aren't supported by Haskell, at least not out of the box.

For example, the vector identity function

f = (...x) => [...x]and

It should take tuples to tuples but since we don't have tuples type in JS, just use the array but the meaning should be the same. Now the correct type annotation might be <...T>, which is a tuple of types of arbitrary length. Is such notation supported by TS? Or any other way to handle it?

That same problem appears for every other variadic operator here, of which there are most.

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Aug 24, 2024

Re. Ava and tape: ah, I see - I think you're right. I forgot that the test-runners contained import-able utilities.

There might be a very hacky way of doing it whereby you add the path containing temporarily downloaded packages to the list of package resolving directories, but it would hardly be worth the time.

Re. The vector identity, it can be annotated like this:

const f = <T extends unknown[]>(...args: T): T

Tuples in JS are Arrays, so this works.

Usage (homogeneous array):

const a = f<string[]>("foo", "bar", "baz")

Usage (heterogeneous tuple):

const a = f<[string, number]>("foo", 10)

@dmitriz
Copy link
Owner

dmitriz commented Aug 24, 2024

In fact, I see the AVA readme says it explicitly:

Make sure to install AVA locally. AVA cannot be run globally.

No motivation or reasoning is provided. Perhaps it is their advertised intent to avoid globals and get everything from their local module instead. These days, jest seems to be the most popular, which does a limited set of globals, which doesn't seem like a major problem. And best of all, it does seem to work via npx, so I am tempted to switch to it, away from ava & tape.

Comparing

  expect(sum(1, 2)).toBe(3)
//vs
  t.is(sum(1,2), 3)

I still prefer the simpler ava syntax achieving the same result with only one method call instead of 2.

About the annotations, I'd like to see how it comes out in practice on a small scale, so if you like to pick one operator that matters most to you and write a minimum possible PR for that operator aiming to achieve the goal you anticipate?

Maybe a separate folder with all TS related files? Additional quality checking npm scripts are fine too, including npx, just no forced package installation please.

Another thing to keep in mind, the addition should not scare away people who don't use TS, so the original code should stay the same but I don't mind to add comments next to the function definitions and in the documentation.

@Lordfirespeed
Copy link
Author

Lordfirespeed commented Aug 24, 2024

My personal preference these days is vitest, but that's primarily because it makes authoring tests in TypeScript a breeze. Since you don't need that, jest is good.

The design of the expect() API is that your assertions always begin with expect(), regardless of whether you are

  • checking whether an object contains certain properties .toMatchObject
  • checking for is equality .toBe
  • checking for deep comparative equality .toEqual
  • or using any of the many other provided matchers

About the annotations, I'd like to see how it comes out in practice on a small scale, so if you like to pick one operator that matters most to you and write a minimum possible PR for that operator aiming to achieve the goal you anticipate?

Would you like me to PR this repository or PR the DefinitelyTyped repo and give you a link ?

@dmitriz
Copy link
Owner

dmitriz commented Aug 24, 2024

I just opened the new issue
#289
on the subject of migrating away from AVA,
to keep it a focused discussion decoupled from the current topic.

I have no experience with vitest but if you have any thoughts about its advantages over jest, you are welcome to add your comments. Also if you see any advantages of the expect() design. :)

As for PR, I would say whatever takes less time to get started and will potentially be simpler and less time-consuming in the future (which simpler things usually are :-)

@dmitriz
Copy link
Owner

dmitriz commented Aug 26, 2024

A relevant PR in the tape repo tape-testing/tape#603

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants