Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

I almost see static typing as an alternative to exhaustive testing. If you're working on a large project written in JS, you need a strong test suite to have any confidence you code will be robust at runtime.

As you say, with ADT's and null safety, you can be a lot more judicious about testing to get to the same level of confidence.

When working with Rust or Swift, I'm honestly usually getting away with some integration tests, and whichever unit tests came about organically when a particular section was particularly well suited to TDD. But "code coverage" is a much less pressing concern.



I would say this is the case even in C++ world. When modern stuff is used correctly (etc. optional, variant, constexpr + static_assert), most of the tests you will ever need are integration tests, basic smoke tests or feature-documenting-tests.


Also fuzz tests! I cannot stress enough how important fuzz tests are if your security model solely relies on your C++ being memory safe (a lot of code should be sandboxed as well but that's another discussion...).

Even when using modern stuff correctly on a mature codebase fuzz testing will find bugs. So if you don't fuzz test and someone evil has your binary then they'll fuzz test for you...


If you are writing a new large project in JS without TypeScript, you are doing it wrong period. Bare JS is a cute language for personal websites but it's not feasible for anything more than a few thousand SLOC.


I'd personally reach for something else than TypeScript. TS tries to type the existing JS world which makes it really big and complex.


The advantage TypeScript has for front-end over something that compiles to JS is that you pretty much know what your code will compile to: with a few exceptions, you just strip out the type annotations. Knowing this, you can accurately predict things like performance.

In contrast, a compiles-to-JS language can produce code that behaves unexpectedly in its non-functional requirements, and in the worst cases crashes where it shouldn't. That will be less of an issue as compilers get better, and WASM should help a lot, but in the current ecosystem TypeScript produces more predictably good results.


That may be true in theory, but I find that people write radically different code in JS and in TS. TS code looks a lot more like C# compared to regular JS code, people use a lot of classes over plain functions and data structures.


TypeScript is not really big and complex, it's just a type system. The only real pain with TypeScript vs. a language with native static types is that TypeScript requires a bit of extra work to fiddle with the compiler, otherwise when it comes to the code it's flexible but can be used in a very simple manner.


> TypeScript is not really big and complex, it's just a type system.

It's a very complex type system though.


Can you say some ways it is more complex than other type systems? I think type systems are inherently complex.

From my perspective, I feel user-facing complexity is quite low in TypeScript in some key ways. You can write your type declarations anywhere, you can import and export them in a familiar JS-module way, you don't need to explicitly type anything in particular so you can be anywhere on the spectrum from very rigorous types to no types at all / `any` everywhere, and finally it has really good error messages so you rarely are stuck with a type mismatch that makes no sense.


Tell that to the mountain of "few thousand line SLOC" javascript projects that existed and continue to exist before Anders Hejlsberg embarked on his mission to C#-ify javascript. I am not opposed to adding strong type checking to JavaScript, but Typescript ain't it.


TypeScript has nothing to do with C#, apart from its creator. The structural type system with explicit nulls, sum types and very powerful generics makes it feel much closer to the ML family of languages than C#, despite using the C-like JS syntax. The teams I've used it in have never really used it to build complex OOP hierarchies or even used classes for anything other than React components, though there are probably many who do.


Both are true. The original TypeScript v1 was a lot more like C#-in-JS and was very class-oriented, however over the years the type system has grown into a mature ML-like structural type system as a side effect of trying to type the wide variety of dynamic JS out there.

Anybody who last looked at TypeScript a few years ago and dismissed it needs to have another look.


I agree that TypeScript leaves something to be desired, but it is far and away a better alternative to pure JS. TS may not be total, but it comes close enough to get the job done for the dynamic environment it's meant to target assuming operators competent enough to lean on what it offers.

I should also note, I've attempted OOP in TS and it wasn't pretty. It is much more suited to a functional style IME, although I will concede I have seen some open source projects that use it in a Java-esque OOP style presumably to good effect. Personally it wasn't my cup of tea.


> I am not opposed to adding strong type checking to JavaScript, but Typescript ain't it.

If you are curious about this, you might be interested in checking out Haxe [0]. It has static typing, type inference, pattern matching [1] (maybe not on par with ML languages, but still good), and can transpile to multiple targets [2] including JS.

[0]: https://haxe.org [1]: https://haxe.org/manual/lf-pattern-matching.html [2]: https://haxe.org/documentation/introduction/compiler-targets...


There are also notable Rescript [1], Melange [2], Reason [3], PureScript[4].

[1] https://rescript-lang.org/

[2] https://github.com/melange-re/melange

[3] https://reasonml.github.io/

[4] https://www.purescript.org/


>Typescript ain't it.

Expand please?


I think what the parent is getting at that Typescript doesn't try to give you the kind of guarantees that something like Rust, Swift, Elm etc. can. They are upfront about this though and it's stated here in the design goals as a non-goal https://github.com/Microsoft/TypeScript/wiki/TypeScript-Desi...


Actually Typescript is it.


If you're wasting your time with needless datatypes you are doing it wrong period.

See what I did there?


Static typing is an alternative to invariants in dynamic languages, not testing.

Testing can't really substitute for types. They're two diametrically opposed approaches to impeding bugs.


They are not diametrically opposed, but rather they’re non-overlapping with a strong type system.

In a dynamic language you end up writing a number of tests that would be covered by a static type system and writing a number of tests that would not be covered by that. Going from dynamic to static typing is a replacement for some of the former, and none of the latter.


>In a dynamic language you end up writing a number of tests that would be covered by a static type system

Not once have I done this. I've written plenty of invariants that would have been covered by a static type system but no tests.

As far as I'm concerned if you had to write an extra test to "cover for a dynamic type system" that test is a bad smell that should be deleted. A test validates that a method raises an exception when being fed a null is pointless if it's just testing a one line assertion that does the same thing. It's a waste of keystrokes. It doesnt catch bugs.

There may be some exceptions to this rule but they would be under very obscure circumstances, I think.


One reason to have tests like that, is to cover regressions. Say somebody changes that method in the future and it doesn’t raise an exception anymore when passed a null (but it is still supposed to, the change introduced a bug).

If you have that “pointless” test, you’ll find out about it. If not, you’ll probably only encounter it at runtime with a program that is subtly wrong.


No, you won't. If somebody removes that line they had a reason. That reason will make them remove the test that makes it fail as well.

Whether it's a good or a bad decision is another matter and not something the test will help with.

Tests that exist to check for the presence of a single line of code are deadweight.


This story doesn't match with my observation programming.

Failure to sanity-check a null almost never stemmed from someone deleting the non-null assertion intentionally. It stemmed from someone rewiring the control flow in such a way as to bypass the non-null assertion without intending to (such as early-returning because they think they can shortcut the computation, but the shortcut isn't actually valid if the argument in question is null).

The tests have value precisely because they aren't in the regular control flow of the unit under test and will fail when the code flow is changed and the author didn't realize the change modified the expected behavior of the function.


> If somebody removes that line they had a reason.

That reason will be one of two things: (1) They replaced it with something they thought would handle the same functionality (along with possibly some other functionality) in a beter way, or (2) The requirement changed.

> That reason will make them remove the test that makes it fail as well.

If it was #2, sure (and in a test-first workflow, they would take out the test first). In case #1, they won’t take out the test, and if their implementation is wrong, the test will let them know.

> Tests that exist to check for the presence of a single line of code are deadweight.

Unless you have a very weird framework, tests don’t check the existence of lines of code, they check the behavior of units. The fact that the first-pass implementation in the same iteration in which the test was added is trivial doesn’t mean the function isn’t part of the contract that needs verified, and doesn’t mean that no one will ever change the intended implementation to a less direct one.


They're deadweight until someone pushes a change that makes some assumption about that line of code the author didn't mention were invalid.


It’s possible that the code was modified in a way that bypasses the null-check. It isn’t necessarily true that anybody removed code.

Unit tests that test the contract of a method (if you put in this argument, then you’ll get this return or side effect) are a good idea in both dynamic and static languages.


>> In a dynamic language you end up writing a number of tests that would be covered by a static type system

> There may be some exceptions to this rule but they would be under very obscure circumstances, I think.

Maybe it depends highly on the specific dynamic and static languages under comparison. My main experience with dynamic languages has been with Groovy (all inherited codebases), and it's an annoyingly frequent occurrence that I've had to deal with PROD issues for things that would have been compile errors in Java.

I believe my favorite is when someone used a String for the counter in a for loop, and it worked...until one day the counter had to go past 9. IIRC, Groovy would convert the String to a char, increment it, then convert that into an Integer. That actually works for 0-9, but incrementing "9" in this way gives you ":" which would then get a NumberFormatException in the last conversion.


> I've written plenty of invariants that would have been covered by a static type system but no tests.

Do these invariants have some language-level support?


> A test validates that a method raises an exception when being fed a null is pointless if it’s just testing a one line assertion that does the same thing. It’s a waste of keystrokes. It doesnt catch bugs.

It catches the bug that gets created when someone does a later refactor, replaces the assertion and some other code with a call to another function that they think throws an exception on null as well as doing some computation needed in the current function, but they are wrong.

Tests validating the contract of the unit are useful even when the trivially pass in the first-pass implementation, because implementations evolve, and the thing people will forget to add tests for with changes are the pre-existing, unmodified expected behavior.

(Also, for workflows where tests are written before the first-pass implementation.)


Yea, I mean the problem is. If you have a function that expects a number parameter. In Typescript, the compiler won't let you do this someFunction('a'), so you never have to worry about handling this. But, if it is in javascript, then someone can call the function like that. So in javascript, you might have a test that ensures that the function handles this gracefully. You obviously don't have to have that test, but you will have your app blow up later down the line, if someone adds a call like that. So, testing does occur at times for these types of things. You might not have done it ever, but there are certainly devs who have, myself included.


How do you express invariants in dynamic languages? (In a way that doesn’t require test coverage to check those invariants?) I thought the only way would be to write some assertions in the prod code, and then write the unit tests to cover them… but you seem to be implying that you can “do” invariants in dynamic languages in a way that doesn’t require testing.


>In a way that doesn’t require test coverage

They obviously work best when "combined with test coverage" but then again, I dont consider test coverage to be "optional" in any language. This idea that "if it compiles it works" that some haskellers seem to believe, for instance, is complete horseshit.

If somebody argues that static typing works great because without tests it catches more bugs than dynamic typing does when you also dont write tests, it's notionally true, yes, but theyre telling you more about themselves than they are about types. Namely that they're not in the habit of writing tests.

If you've got enough of the right kind of testing coverage to validate an app written in a statically typed language you will have enough to trigger invariants in a dynamically typed langauge.

I have successfully had invariants usefully triggered in prod and by manual testers in big balls of mud without test coverage (failing fast isnt just for test environments) but I considered that to be a form of emergency programming CPR and damage limitation that isnt a substitute for fixing your testing "diet".


Agreed; "if it compiles it works" tends to tilt too far in the other direction.

(... I once had a professor who believed that phrase as nearly a point of religion, and his project work was maddening. No documentation on any functions either, because "the types are self-documenting." Then you'd get hit with a 'division' function that takes (number,number) but he put the denominator as the first argument. Ugh.)


I'm not an "expert" at dynamic languages, but I've written small microservices in Clojure and Elixir, data crunching code in Python, and your typical junk in PHP and JavaScript.

Clojure does have spec (https://clojure.org/about/spec) which is pretty cool and declarative. And, of course, your IDE can help you when writing code that obviously violates a spec declaration. But that's not fundamentally different from an IDE parsing the type-hints of Python, JavaScript, or PHP. It is more powerful than the aforementioned type-hints, though.

At the end of the day, it blurs the line between static typing and dynamic typing. IMO it should be considered static typing because it serves that same purpose, works at "write time" to help the programmers, and can probably absolve you of writing your own type-checking tests.

In some sense, it is all just asserts. Even statically typed languages have unsafe type casting, which can trigger an "assert" at runtime.


I guess you can sort-of use design-by-contract and call it an alternative?




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: