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

Consider using ts-semantic-testing #2

Closed
SamPruden opened this issue Aug 14, 2017 · 82 comments
Closed

Consider using ts-semantic-testing #2

SamPruden opened this issue Aug 14, 2017 · 82 comments

Comments

@SamPruden
Copy link

I've just published an early version prototype of an idea to help with writing tests for these kinds of projects, ts-semantic-testing. It's still very early days and potentially buggy and all subject to change, but I thought you might find it interesting to have a look at.

@KiaraGrouwstra
Copy link
Owner

TypeScript: turn run-time errors into compilation errors

ts-semantic-testing: turn compilation errors back into run-time errors

😅

Honestly, I love this -- it's heart-warming this experiment to do a type lib is already spawning tooling specific to this niche. I'll try it out.

@SamPruden
Copy link
Author

Have fun, feel free to request things or contribute or whatever if you'd like.

I've actually been thinking I may completely change around the API though, to something roughly like this. Basically break it away from transforming the full test files and tests, and just provide a single block that is transformed and makes any semantic errors available for assertions, in a completely test framework independent way.

it("uses the new rough API idea", () => {
    tsst(() => {
        // Semantic errors within here are returned as strings
        // Only the contents of tsst are transformed
        type A = B;
    }).expect.toNotFindType("B");
});

So feel free to play around with it, and use it if it seems useful in its current state, but beware this is all very prototype right now and will change drastically.

@SamPruden
Copy link
Author

Oh, and while it's designed for this niche, I do wonder if semantic tests may sometimes be useful in more general contexts. The organisation that comes with a testing framework may be a nicer experience than just looking out for compile errors in some situations.

@KiaraGrouwstra
Copy link
Owner

Features looked for to test types (also see sanctuary-js/sanctuary#254 (comment)):

  • testing things type-check: originally filled by TypeScript
  • testing for expected errors: tested within TS by its harness, outside of it originally by dts-lint
  • protecting against any inference: tested within TS by its harness, outside of it originally filled by typings-checker
  • getting version control friendly (= no line numbers) error logs for libs that still failed expectations: my typings-checker fork (used for Ramda typings)
  • combined testing of types and run-time results: dts-jest (used in impending PR to Ramda typings)
  • turning type errors into run-time errors to make named assertions: ts-semantic-testing

Seems to me that for this use-case you already had the right feature-set in mind.

I do wonder if semantic tests may sometimes be useful in more general contexts. The organisation that comes with a testing framework may be a nicer experience than just looking out for compile errors in some situations.

I'd be curious about its potential as well. Then again, normally people make tests to prevent regressions from sneaking in, whereas it's harder for compilation errors to sneak in without noticing upon compilation. In that sense, use-cases elsewhere might be more limited.

On tsst, so current API:

it("works with the number 0", () => {
  type A = ZeroOneToBoolean<0>;
});

What I had now in my attempt to test without names: the<number, 123>();

Imagined way the syntax could collapse:

it("is a number", the<number, 123>);

... Which would fail since generics for functions could only be provided upon calling them. They can be provided for types, but we want an expression-level function here. I guess one could be like

let x = null!; // short expression-level `never`, can be cast to anything
type TheF<T, V extends T> = () => V; // type-testing function type to lift to expression level
// options to invoke without calling function:
x as TheF<number, 123>;
<TheF<number, 123>>x;
// imagined test:
it("is a number", <TheF<
  number,
  123
>>x);

More of a thought experiment than pretty syntax. Also fails to yield a function expression on type error though which I suppose screws it up.

Do you have an example project you've actually already used it on by the way?

@KiaraGrouwstra
Copy link
Owner

This project is currently horrendously untested because I'm not sure what the best way to test this is.

Put your example snippet in some sub-folder 'repository' and have a script calling it on that folder? Maybe have your CLI propagate the status code of the testing thing so you'd know where the testing thing reported errors? You could e.g. have one test project like that that should pass, another that should result in some error so as to check whether it'd correctly report success/failure based on your transformed types, I dunno.

@KiaraGrouwstra
Copy link
Owner

issues with line-numbers

Hm. If the it blocks already describe what failed, then a work-around may be to limit it to one type per block or something.

I suppose if you could put your own example project for this up there it might serve simultaneously as both documentation on usage as well as a basis for testing 😅, right now running tsst doesn't seem to have it tell much as to what it's doing (is it outputting transformed files somewhere?), which is a bit confusing.

@SamPruden
Copy link
Author

I'll work on getting a proper example out soon sure, that sounds good. And currently tsst is (theoretically, it was a 10 minute job that at least works locally for me) just running the compiler against your tsconfig.json in pretty much the same way just running tsc would, but only emitting files where the source file matches the glob.

It's literally just this:

    const projectPath = project || "./";
    const configPath = ts.findConfigFile(projectPath, ts.sys.fileExists);
    const basePath = path.resolve(path.dirname(configPath)); /*?*/
    const configReadResult = ts.readConfigFile(configPath, ts.sys.readFile);

    if (configReadResult.error) throw new Error("Error reading tsconfig.json");

    const config = ts.parseJsonConfigFileContent(configReadResult.config, ts.sys, basePath);
    const program = ts.createProgram(config.fileNames, config.options);
    const transformer = makeTransformer(program);

    program.getSourceFiles()
        .filter(file => minimatch(file.fileName, glob))
        .forEach(file => program.emit(file, undefined, undefined, undefined, {
            before: [transformer]
        }));

@KiaraGrouwstra
Copy link
Owner

Thanks! That's illuminating, let me try for a sec then.

@SimonMeskens
Copy link

This stuff is great, love to see progress in this space.

@KiaraGrouwstra
Copy link
Owner

KiaraGrouwstra commented Aug 14, 2017

Oh, I installed a local version figuring globals are an anti-pattern with version control, then erroneously tried running index.js instead of build.js, so that explains why mine silently terminated.

Edit: not outputting code, even with noEmit: false. I'll await the example mini-repo for a fool-proof baseline. :D

@SamPruden
Copy link
Author

SamPruden commented Aug 14, 2017

Oops, sorry. Yeah, I'm working on the example now, it's a little tricky to work out how to nicely integrate with an existing test framework because you have to do the transformations before the tests... I think the version I put up will be working but a mess, will have to find/build a better way at some point.

Just to be clear about running the build, tsst is a bin in the package. So if you've installed the package globally you should just be able to run tsst on the command line, or add this in the package.json and npm test.

"scripts": {
    "test": "tsst **/*.test.ts"
}

@SamPruden
Copy link
Author

I've clearly explained that awfully, sorry. I'm prototyping/iterating significantly faster than I'm documenting.

@SamPruden
Copy link
Author

Sorry, I ended up mostly having to deal with some other boring stuff today, didn't get as much time to work on this as I would have liked. https://github.com/TheOtherSamP/tsst-example-project is now a thing, I think it works.

I've been having a slight issue installing it, I've been intermittently getting a weird

npm ERR! enoent ENOENT: no such file or directory, rename 'D:\tsst-example-project-master\node_modules\.staging\tsst-36171f51\node_modules\@types\minimatch' -> 'D:\tsst-example-project-master\node_modules\.staging\@types\minimatch-b98dca64'

error when npm installing in the project directory. I think this is probably local to my system (I've had a few other problems with NPM, I need to clean that up) but if this happens to you, I've managed to get around it by manually npm install tsst, then npm install. No idea why.

@SamPruden
Copy link
Author

Oh, and I completely changed the syntax around, surprise!

@KiaraGrouwstra
Copy link
Owner

KiaraGrouwstra commented Aug 15, 2017

Hm.

  • I also get that npm issue. Thanks for the workaround.
  • npm test then yields /usr/bin/env: ‘node\r’: No such file or directory for me (Ubuntu) due to the bin file using Windows breaklines (CRLF) over Unix ones (LF). For me VSCode shows an option to fix that in the bottom-left corner, allowing me to get to the next stop. For a long-term solution, it appears that Git automatically fixes CRLF to LF, while npm does not. I wonder if you could publish to npm again after setting your local editor to use Unix breaklines?
  • got some Determining test suites to run...(node:5680) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): SyntaxError: Invalid regular expression: /js-tests\(.+)\.test\.js$/: Unmatched ')' apparently due to old global install, fixed when using local install. this is why I prefer local installs, less version mismatches. :b
  • trying node_modules/.bin/tsst -b tests/*.test.ts -d ./tests terminates silently. minimatch appears to reject the global, file.filename /path/to/tsst-example-project/tests/operators.test.ts, glob somehow tests/operators.test.ts, minimatch then yielding false.
  • not sure if it can make that js-tests dir for you, probably blocked by the above

Edit: incidentally '/path/to/tsst-example-project/tests/operators.test.ts'.includes('tests/operators.test.ts') and '/path/to/tsst-example-project/tests/operators.test.ts'.match(/tests\/operators\.test\.ts/) both work. I gues the weird part seems how my glob somehow got filled out. I thought it might be cuz it was the only matching file in there, but adding another matching file doesn't seem to prevent it from becoming tests/operators.test.ts.

@SamPruden
Copy link
Author

Huh, I'm on Windows and don't have easy quick access to a Ubuntu environment for testing right now.

I should probably mention that I'm more of a .Net dev historically, I may be making silly mistakes here, I'm learning as I go, sorry.

not sure if it can make that js-tests dir for you, probably blocked by the above

Works for me.

I also get that npm issue. Thanks for the workaround.

Strange. I'll clear everything out and see if I can work out what's going on there. I don't think I've done anything particularly weird with NPM here, so not sure what's up with that.

I wonder if you could publish to npm again after setting your local editor to use Unix breaklines?

On it now, sorry. I should have had that set already, my bad.

trying node_modules/.bin/tsst -b tests/*.test.ts -d ./tests terminates silently.

I'll throw some logging in there quickly. I'm not immediately sure what's going on with that error either. Could that be a Unix thing with path handling somewhere? Apart from the NPM thing, just copying down the folder from github and running locally just works for me.

Aaand this already became a support job. 😂

@KiaraGrouwstra
Copy link
Owner

KiaraGrouwstra commented Aug 15, 2017

Nah it's good. I only switched over half a year ago when I realized my most-wanted Windows 10 had become better BashOnWindows support. We're all learning anyway.

I'll try to figure out what the heck is going on with that glob (edited previous post a bit but still on it). Other two should be fixed with LF and using local install.

@KiaraGrouwstra
Copy link
Owner

Sorry, seems quotes matter for me, my bad.

node_modules/.bin/tsst -b tests/*.test.ts -d ./tests
glob: 'tests/operators.test.ts'

node_modules/.bin/tsst -b "tests/*.test.ts" -d ./tests
glob: 'tests/*.test.ts'

So file name /path/to/tsst-example-project/tests/operators.test.ts, glob tests/*.test.ts, minimatch still reports false. Hm.

@KiaraGrouwstra
Copy link
Owner

My bad, swapping tests back to ** fixes the check.

@SamPruden
Copy link
Author

Hmm, but I think that should have worked. I just threw the standard minimatch in there and assumed it would just work, but that does seem to be an issue, maybe I need to mess with options for it or something. I'm actually seeing the same thing here when I set that to tests/*.test.ts.

Nothing's ever simple. 😂

@KiaraGrouwstra
Copy link
Owner

Transpilation now works fine. Running jest still throws the regex error but that might be some version thing. Checking.

@SamPruden
Copy link
Author

No, that's actually just a buggy regex I wrote. Your error is write, the confusing thing is that it works for me.

@KiaraGrouwstra
Copy link
Owner

Thanks, yeah, just saw it. Made a PR, probably at the same time as you fixing it haha.

@KiaraGrouwstra
Copy link
Owner

Uh, actually, that 'fix' is wrong. Would "js-tests\/(.+)\\.test\\.js$" work on Windows? I'm unsure due to the slash. Works for me.

@SamPruden
Copy link
Author

I've been awake for about 30 hours and drank too many coffees, try not to judge me too harshly for how much of a mess I just made of the commit history. No, blame me, that's fair. 😂

@SamPruden
Copy link
Author

Actually I'm going to disappear and sleep now before I accidentally destroy this whole project.

@KiaraGrouwstra
Copy link
Owner

Get some rest, you deserve it 😃, I'll try to see what I can do to incorporate this over here! Heck, I'm pretty sure @gcanti should be interested too!

@SamPruden
Copy link
Author

Have fun, thanks for all the help. If I don't spend the whole day battling the weird NPM issue I think I'm going to try to get a basic expectation/assertion system in it tomorrow. And fix the millions of bugs I've probably sleepily put into it today.

@KiaraGrouwstra
Copy link
Owner

Yeah, I'll try a bit as well, expect another PR or two.

@KiaraGrouwstra
Copy link
Owner

KiaraGrouwstra commented Aug 15, 2017

Note that the setup of tsst-example-project allows one to have tests import types, one cannot import non-type TS (like the) with it.

I tried to chop the .d off the operators.d.ts file name, which makes the behavior of tsst magically change -- suddenly it dumps the resulting JS files not in js-tests, but in js-tests/tests.

Moreover though, as the regular TS compilation process is skipped, meaning it will transpile the import statements, which will then no longer have anything to import from.

Being able to import the from tsst might be one workaround there.

Also tried for a bit to see where I could take this:

tsst(() => {
  the<number, 123>();
}).expectToCompile();

... to something terser too (now I can actually test), but not much luck without touching transformers at least.

@SamPruden
Copy link
Author

As a target, I'd also like to be able to compare an object instance against an interface to assert that return values really do conform to their types, but I think that's probably fairly far off still for now.

@SamPruden
Copy link
Author

By "fairly far off", I may well mean impossible. Things like fully checking the return types of functions will likely never be possible. That idea is still in the early stages of wouldn't-it-be-nice-if, don't expect it to become reality.

@SamPruden
Copy link
Author

Although, if you fancy putting in a PR that solves the halting problem, I'll get right on that.

@KiaraGrouwstra
Copy link
Owner

Return types feels hard, yeah -- if it's based on generics things quickly get hard to track in a generic way. I mean, try that for Ramda's path function. There's like nothing useful you can say about its return type in a general sense really.

The halting issue... so so far I got to have it crash only a single file rather than the whole script, which helped. A better solution... probably involves parsing the whole project without type checking, then separately checking the types in those tsst nodes?

Hopefully something like this to further quarantine the halt to the test level, rather than the file level... It sounds like that means there would no longer be file-wide type checks as before though, which... sounds... kind of breaking / different.

@SamPruden
Copy link
Author

Oh, I'd completely forgotten about that halting issue, hah. I was actually just trying to make a bad joke about the impossibility of determining the return type of an arbitrary function because of the halting-problem in the comp.sci sense, but I do need to look into that TS problem too.

Yeah, we definitely need to type-check the whole thing, otherwise the tests are pretty much completely pointless - they'd only be able to check types defined within themselves. I think ultimately, other than crashing gracefully, dealing with that issue is out of scope. I see that as a TS crash that's on them to fix. Best case I might be able to detect it and suggest what's happened in the error message.

In terms of the comparing against interfaces idea I suggested, my current thinking is that it's probably theoretically possible with most things apart from functions, because we can't test what happens for every possible parameter or state. The best idea I have right now is somehow convert arbitrary objects into a form the compiler understands (maybe impossible) and pass them in and do checking by hacks similar to how I'm currently doing is<T>(). Realistically this is likely unachievable.

@SamPruden
Copy link
Author

I still haven't looked into it, so this may not make sense, but I'd assume the best way for TS to solve the problem would be to fail as a type error if it detects that it can't resolve the type. It would be a weird (but hopefully rare) error for developers, but it's probably the best option unless the problem can be eliminated.

@KiaraGrouwstra
Copy link
Owner

Yeah, we definitely need to type-check the whole thing, otherwise the tests are pretty much completely pointless - they'd only be able to check types defined within themselves. I think ultimately, other than crashing gracefully, dealing with that issue is out of scope. I see that as a TS crash that's on them to fix.

Yeah. I thought they already had some similar errors for types that seemed 'too' recursive. It definitely isn't practical to resolve from our end here.

I'd like to be able to compare an object instance against an interface to assert that return values really do conform to their types

So bridging run-time (JS) and compile-time (type) testing huh?
Sounds like we'd have a Jest test involving some return value, which we'd then parse to infer its type, and add a tsst 'type-level' test checking if that inferred type matches the interface the function returns. Sounds... doable?

@KiaraGrouwstra
Copy link
Owner

By the way, short-term gains might be to just npm publish a current (pre-refactor) version, optionally git install proof so you wouldn't need to npm publish every single commit to make them consumable. :)

@SamPruden
Copy link
Author

My intuition is that it's doable for the simple cases, but impossible in the general case. Which arguably makes it useless.

which we'd then parse to infer its type

Doing this for a JSON-serialisable object is easy. Doing this for any object is hard. Anything that can't be represented by an object literal, I think we're going to hit a roadblock with.

If we can't reliably assess any object that we feed into the test then the test is pretty much useless.

We may be able to make it work for any object, but only for a subset of all possible interfaces. At that point it's still a hassle, of limited use, and probably not worth it.

@SamPruden
Copy link
Author

By the way, short-term gains might be to just npm publish a current (pre-refactor) version, optionally git install proof so you wouldn't need to npm publish every single commit to make them consumable. :)

Yeah, I'm not actually even at the machine right now so I can't do this instantly, but I'll set myself the goal of getting a version up within 24 hours. Don't be surprised if I fail that goal though.

@SamPruden
Copy link
Author

Although I'm not 100% sure I can't take an arbitrary object and reconstruct a TS compiler Type object piece-by-piece. That's getting pretty insane, but it might be theoretically possible. We'd still never be able to fully check return types or anything that wasn't deterministic.

@SamPruden
Copy link
Author

Chances of the TS compiler actually exposing everything I'd need to do that at the moment are very low, too.

@KiaraGrouwstra
Copy link
Owner

Doing this for any object is hard.

Isn't TS's raison d'être to gradually get to this point? Might be worth it to try and see how far off it ends up.
I might be missing what you're getting at -- would you have examples of hard cases?

Although I'm not 100% sure I can't take an arbitrary object and reconstruct a TS compiler Type object piece-by-piece. That's getting pretty insane, but it might be theoretically possible.

I suppose you did have the step of parsing a string into a node, if for a whole file?
Can't contain variables though or it'd no longer have meaning if parsed on its own.
Getting the type is createTypeChecker -> getTypeOfNode.

@SamPruden
Copy link
Author

parsing a string into a node

Yes, this is definitely possible, I'm doing it now. Admittedly my current way of doing it is not ideal for units smaller than files (I create a SourceFile from string) but while a little silly and inefficient, it's perfectly possible to just do that and then extract the node. If you want full type checking in it, I think you have to do a weird round-about thing where you load it into the program as a fake file though. Again, I'm doing this now and it works, but it's ugly.

Simple example of a hard object:

const hardObject = {};
hardObject.self = hardObject;

If we just have variable hardObject and no knowledge of how it was created, there's no (easy) way to convert this into a string that the parser...

*thought happens*

Oh, okay, actually maybe there is.

For some reason I was thinking in terms of converting the object into an object literal in string form (so basically JSON but with basic function signatures) and giving that to the compiler, but actually it may well be possible to take a runtime object and produce a typescript code for an interface of that object.

So the hardObject above would just be:

interface $$tsstGeneratedInterface1OrWhatever {
    self: $$tsstGeneratedInterface1OrWhatever;
}

Maybe this is what you were thinking all along. Okay, so it now seems more achievable. There are still problems though, particularly around generics.

  • Function signatures will never really be possible to get right. Probably (...any[]) => any is the best we can do. Maybe we could use .length to help here slightly, but not much, and there are problems with that.
  • What type do we give generic collections? We could probably make Array<T> work by checking every element at runtime, that seems okay to have a special check for. But what about Map<K, V>, should we write a special checker for that too? And Set<T> too? What about custom collection classes people may write? The best we can do with those in the general case is make things any, but that could lead to incorrect results sometimes.
  • Any type that's distinguished by function signatures (as for example Map<K, V> is) will sometimes yield wrong results unless it has a specialised checker implementation.
// The best interface we could possibly generate under ideal conditions for a Map<K, V> without
// special code that knows to check the keys and values properties and set signatures accordingly.
// Comparing this against Map<string, number> is useless, it can't check those generics
interface $$tsstGeneratedInterface2OrWhatever {
    // More realistically this would be `get(...args: any[]): any;`
    // Ideally we could sometimes get some info from function decompilation
    get(key: any): any;
    set(key: any, value: any): any;
    ...
}

So, we can do a bit more than I originally thought, but I'm still pretty sure that the lack of signatures means a lot of real world scenarios will fail. If it can't be relied upon to work for everything, it's effectively useless.

@KiaraGrouwstra
Copy link
Owner

Fair enough! Admittedly recursive objects hadn't really occurred to me. Admittedly in those cases things may get tougher. I'd argue even before reaching perfection this may still be useful though. Same as TS itself!

Functions

I'd suggested generic inference in #14078 before, which @masaeedu incorporated into a more pronounced proposal at #17428. So hopefully that'll improve, though perhaps the bigger problem for functions isn't even so much a TS one, but rather JS level function comparison. I imagine that's identity-based, which fails for testing purposes I suppose.

In that sense, I suppose realistically functions will likely end up having to be tested based on their results in practice, i.e. testing not the function directly, but a few applied versions so testing becomes doable again both for JS as well as for type inference purposes.

What type do we give generic collections?

So in these interface-value matching checks, the interface can be regarded as a LHS, the value as a RHS. That means in order to get the most realistic check, we'd prefer for the inferred type on the value to be as granular as possible, i.e. no widening if it can be avoided. This means we wouldn't infer Person[], but could infer [{ name: 'abc }, { name: 'def' }], i.e. a type that's essentially the value expressed in type literals. And that's fine, because we can use just that to match the interface.

Map / Set may be tough as their imperative implementation may not give much inference akin to the type literal representations of plain arrays/objects. Still though, doesn't need to be perfect to improve what we have now. :)

@SamPruden
Copy link
Author

the bigger problem for functions isn't even so much a TS one, but rather JS level function comparison

Absolutely. The restrictions aren't in TS, they're in JS. The whole point of TS is to add extra type information that JS doesn't have, and we can't recover all of that from JS objects. That's unsolvable unless JS add type information.

doesn't need to be perfect to improve what we have now

I don't know, I think it might have to be. I don't personally see much value in a test with unreliable results.

Let's say we have the following interface somewhere in our source:

interface Foo {
    bar: string | number;
}

Okay, we can test that fine.

const foo = someFunctionThatShouldReturnFoo();

// Not what the actual syntax would be
expect( tsst(foo).is<Foo>() ).toBeTrue();

That works, we should be able to make tsst validate against that interface. But now let's say that we alter the code, and add a baz: Map<string, string>; property to Foo, but we accidentally return a Map<string, number> object as the property in someFunctionThatShouldReturnFoo.

Well now the test still passes because tsst can't access all the type information in the Map and we have a test that's incorrectly passing. In my book, a falsly passing test is worse than no test. We're really just trying to re-check what's already being done by the type checker here. It would be nice if we could, but I don't think it's possible.

@KiaraGrouwstra
Copy link
Owner

The restrictions aren't in TS, they're in JS. [...] we can't recover all of that from JS objects.

fwiw, from (a, b) => a + b TS can infer... arity. the main JS problem isn't even this inference part though. it's that in Jest you can't just be like expect(fn).toEqual((a, b) => a + b) -- at run-time this would check identity-based equality, which isn't useful for the purpose of the intended check.

So there's a limitation there, but it's not even imposed by your additions. And heck, no-one's claimed Jest to be useless over this limitation yet. So essentially functions would instead be tested indirectly I guess. That works.

In fact, for ES6 Set/Map the exact same limitations apply -- the comparison doesn't even work at Jest's run-time as JS would apply identity-based equality checks.

Perhaps when we get something we can't compare (function/Set/Map) we could give some warning notifying the user part of the test is not meaningful (for either run-time or compile-time checks!). Functions could still be tested in separate tests applying them.

Admittedly sucks for Set / Map users, but given its imperative API it's been anathema for static type checking to begin with. I'd argue that's not on TS. Seems there was intent to improve on that at gcanti/fp-ts#161, fwiw.

@SamPruden
Copy link
Author

Hmm...

Throwing up warnings or failing when the checked interface doesn't comply with our restrictions is an option, but I think it would quickly get too abrasive for me to ever use. The interface/object comparison check idea is really just a convenience for testing the same things we currently check manually property-by-property using standard testing procedures. If we had to worry about tests breaking and having to be rewritten if we choose to add one property to an interface that would completely negate the convenience for me.

@KiaraGrouwstra
Copy link
Owner

Hm. Perhaps the reason I didn't feel as reluctant is I've personally largely avoided the ES6 structures (and say Immutable.js) in favor of plain structures, mostly since those seemed to work nicely with the functional paradigm offered by Ramda that static type checking kind of depends on.

Following that paradigm (separating logic and data), I wouldn't normally mix functions into such object structures either. In that sense, my own style would likely not have suffered much from these limitations.

I think the good part here is that JS limitations already means many of these cases would already have stayed out of Jest tests, but yeah, not Set/Map I guess.

@SamPruden
Copy link
Author

That's fair. It's worth noting that it's more than just Set/Map though, it's any object with function signatures at any depth. I think, putting it like that, that's a large swathe of all objects. Personally I'd feel uncomfortable writing a test that relied on that assumption unless I could guarantee that assumption would continue to hold as the codebase developed, too.

The basic problem is still one I'm interested in solving, but I'm not convinced this is the solution.

@KiaraGrouwstra
Copy link
Owner

On second thought... the checks might be in place already. expect(fn).toEqual((a, b) => a + b) always fails at run-time due to identity checks, meaning no-one is likely to keep testing in this way 'any object with function signatures at any depth'. Note that prototype methods won't count btw.
So hopefully you have no more constraints than those already pre-imposed by the run-time side.

@KiaraGrouwstra
Copy link
Owner

If I may bounce off an idea since you have a bit more experience here, could I say parse a typings lib and get its typings for different exports?

Use-case: use function info to allow the user to give a few params, and filter down the list of functions to those usable for those inputs.

I figured some FP libs like Ramda primarily face adoption issues from the learning curve of their overwhelming number of functions, so I figured it'd be cool if I could help users a bit find the functions they need.

@SamPruden
Copy link
Author

Sorry, yet again something came up and I haven't got this done. I think I may have the time over the next couple of days, but I've learned not to make that commitment.

Use-case: use function info to allow the user to give a few params, and filter down the list of functions to those usable for those inputs.

That's an interesting idea, and I think the answer is yes, partially.

Pulling out all of the exports shouldn't be hard. TypeChecker.prototype.getExportsOfModule(moduleSymbol: ts.Symbol) exists, although I haven't used it. Otherwise it shouldn't be too hard to just traverse the file and find all of the export declarations. Filtering these exports down to the functions shouldn't be a problem.

You can get the signatures out of a function in a few ways, including from the type and from the declaration nodes. So getting a list of function names and their signatures shouldn't be too hard.

The place where it gets a little tricky is filtering those signatures. (str: string) => void shouldn't be too hard to deal with, but how you want to handle filtering of more complex parameter types I'm not sure. If you have a parameter type of string | number things are already a little tricky.

What format were you expecting the user to give params? Actual values? Strings representing types? Would this be running in an environment where you could use the compiler itself in your filtering interface?

@KiaraGrouwstra
Copy link
Owner

Thanks! That seems exactly what I was looking for. :)

how you want to handle filtering of more complex parameter types I'm not sure.

This part I'd considered already. I fear this becomes just brute-forcing the potential positions of known input parameters (or rather, their types) to check if any work. :)

Would this be running in an environment where you could use the compiler itself in your filtering interface?

I'd imagine the TS compiler would be loaded into the browser here, yeah. I looked at JSDoc data as an alternative way to check function signatures (over TS), but those seem to lack even basic meta like function names.

What format were you expecting the user to give params? Actual values? Strings representing types?

Values. The idea would be to just execute the legitimate options so the user could just see the actual results by function (+ param order, if multiple match).

@SamPruden
Copy link
Author

Well good luck, and be warned that matching the signatures may prove quite hard, I'm not sure how I'd do that. The language service also provides some methods for autocompletion that may be of interest here. I don't think that would give you the info you want directly, but the source of methods like getCompletionsAtPosition might be worth a read.

@KiaraGrouwstra
Copy link
Owner

Tried for a bit, but I did still run into bumps, yeah.
Could you publish a recent-ish version of tsst to npm again? Then I'll put that as the dep here.

@SamPruden
Copy link
Author

Right, yeah, sorry. I've had about four other high priority projects going on and couldn't particularly justify throwing time into this fun side thing, but you're waiting on it and I said I would so I'll get something out today/tomorrow. I'm actually just heading to bed, but I'll work on this first thing and get out a basic MVP.

@KiaraGrouwstra
Copy link
Owner

Eh, if I needed something changed I should just try and add it myself, it's just the npm repo isn't mine to publish to. :)

@SamPruden
Copy link
Author

Hey, so just an update because I'm late again. I went to do that and found there was a significant bug still in it that I'd forgotten I'd only got half way through fixing, so it wasn't actually functioning at all. It's a little more work to get a releasable version than I'd thought, but I am actively working on it when I can. I'll tentatively say a day or two.

@KiaraGrouwstra
Copy link
Owner

Would it be doable to just git checkout the version before the refactor and publish that? I hope that should help alleviate pressure on your end.

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

3 participants