Bad TypeScript Habits
startup-cto.net
startup-cto.net
That's one thing I like about T, it's very self evident that it's just 'the' generic type. But maybe I'm just used to it. With multiple generic types I can see how longer names would be useful.
`Element` is as non-descriptive as `T` in the author’s example and, IMO, doesn’t add any tangible benefit.
The latter scenario being that hopefully the person debugging your logic a decade from now has some insight into what you were thinking, namely.
public delegate TResult Func<in T,out TResult>(T arg);
(Java has others like just like how every time you open a Gradle project you can expect someone has invented yet another unique way of structuring build files for this repo. :)
I started off in .NET and when I was using Java it wasn't immediately obvious what types were interfaces or not (e.g. "List" is an interface in Java, and also especially when the interface suggests what the implementation is).
C# can be described as a considerably better Java - but I wish they didn't inherit Java's interfaces vs. classes model and instead had something that enables some form of structural-typing: something like TypeScript's interfaces or Swift's protocols.
That said, a bit more about why the I feel the I convention is unneeded and even a problem:
- If one doesn't use a powerful IDE when using either C#, Java or even Typescript there is a fair chance one is doing something wrong. (I allow for exceptions for people who are so insanely smart that they need the lack of support as a brake for their brains ;-) Powerful IDEs can tell you just fine what is a class and what is an interface.
- In what I think of as well written Java projects (and C# many projects for that matter) you'll often feel what is an interface and what is a list just by the name anyway: Javas List that you mention is a perfect example: List is the general concept of a List in Java. ArrayList, LinkedList etc are implementations of that. Once you get used to this convention it is hard to even recognize it until you sit down and try to explain it to someone.)
- Contrary, prefixing with I (typical .Net style) or suffixing with Interface (something I've seen in older Java projects) these days[1] encourages lazy naming and unnecessary duplication: If there is only one interface, just write a class and let the interface be implicit. Especially in Java, but also in C# and Typescript refactoring is so trivial (unless you are changing an external API that other people already depend on) that you can just create an interface with the same name and rename the class later if you need it; there's no reason to worry about it up front.
[1]: "these days" added as a qualifier since it used to be that certain older systems required you to write one or more interfaces for each class.
Yes, but if one is simply reading code from a book or code sample online no such IDE benefit exists. Program code should be comprehensible as plaintext. This is why I'm not a fan of overuse of the `var` keyword in C# - or `auto` in C++. (Many people feel forced to use `var` and `auto` in C# and C++ respectively when their program uses horribly complicated generics or template instantiations - this wouldn't be necessary if C# permitted more flexibility with type-aliases.
> List is the general concept of a List in Java
But that's a huge design-issue in itself: Interfaces should not describe what something "is": they should describe what something is capable of - this is doubly essential when using reified generics because of type parameter variance (Java skirts this one because of type-erased generics - which is another discussion). A distinct advantage of interfaces describing capabilities is the "I" prefix can be replaced with a rule requiring interface names to be adjectives (i.e. class/struct = noun, method = verb, interface = adjective+noun).
Rather than having this (invariant) interface:
interface List<T> {
void add( T item );
void remove( T item );
int length { get; }
T get( int index );
}
We'd have these variant interfaces: interface ReadableList<out T> {
int length { get; }
T get( int index )
}
interface MutableBag<in T> {
void add( T item )
void remove( T item )
}
Which is great because then we can do type-safe stuff like this: class Vector<T> implements ReadableList<T>, MutableBag<T> {
// etc
}
class Employee extends Person {}
class Boss extends Employee {}
Vector<Employee> list = new Vector<Employee>();
ReadableList<Person> people = list;
MutableBag<Boss> appendOnlyBossesFromHere = list;If you have Thing as the Interface and ThingImpl as the class you should probably delete the interface and rename ThingImpl to just Thing.
If you really really know multiple implementations are coming down the road you can name the class DescriptionClass (like ArrayList and LinkedList), but generally these days I'll just cross that bridge when I get there. (Always be careful when working on external interfaces though.)
Historical note: In ancient (in a web perspective) Java enterprise edition (not to be confused with modern Java EE) this style was encouraged and even enforced. Today it is widely considered an antipattern. Last time I remember a project where this was how it was supposed to be done was probably back in 2008 in a Spring/JSF project. Both Spring and JSF have changed a lot since then.
Forget I<Whatever>. It breaks sorting and it is allows people to avoid thinking if the interface is necessary or not.
Second:
I'm not one of the hardliners wrt avoiding mocking, but if every class needs to have a corresponding mock then I'd suggest taking a step back because that shouldn't be necessary, and often in such cases I think one will also find lots of low value or even no-value tests.
I'll not judge you based on a comment on an internet forum, but I think the above is a good heuristic.
(With TypeScript and Java closures or anonymous types is also an option and something exist in C# as well.)
100%. Any "structure definition" should be assumed to be an interface, and any explicit separation of definition and implmentation should be deferred until multiple variants are required. My Java projects and files were corked up with Ifiles that only ever had a single implementation.
Caveat: I've only worked on internal projects, explicit interfaces are much more useful for something widely shared. Don't use the I<Class> convention though.
Rust convention is to mostly use this style of single-letter generics, but not to shy away from using longer names like regular type where useful (but not to prefix them like TFoo).
All these shorthands (e.g. K,V = Key,Value) have to be learned so should be used sparingly but in some places I think having shorthands for things like generics & indices allow boilerplate code to "get out of the way" of the business logic.
You expend less mental energy trying to keep track of "i" (as you know it's a simple loop counter) than "index". And reducing cognitive load is the point of having descriptive names in the first place.
Code that has more characters just takes longer to read over and process as well e.g. having to mentally match up which variable names are the same and spotting patterns. Yes, use descriptive names where it helps which is the vast majority of the time, but for small functions where from context it's obvious e.g. `i` can be better than something like `customerReportIndex`, as well as your T example for a general type. Maths proofs would be tiring to read with long names for all the variables for example.
fn square(numberToBeSquared : number): number {
return numberToBeSquared * numberToBeSquared;
}
The one letter variable version of the function is perfectly readable. Most generics aren't used in a significantly complicated way. Sure, maybe if you're doing something like Haskell's lenses, then you should use descriptive names might be better, but how often are you writing type Lens s t a b = forall f. Functor f => (a -> f b) -> s -> f t
and is type Lens source dest gettable settable =
forall structure. Functor structure =>
(gettable -> structure settable)
-> source
-> structure dest
really any easier to read?When I see a lower case single letter, I assume either a loop control or an enumerator var.
When I see an upper case single letter, it's a generic.
I recently switched back to JS for a prototype of a programming language.
I immediately felt more productive.
At times, the TS systems feels like a theorem prover. Until it doesn't. And then you're left with a bunch of types that almost do what you want... but not quite; Always tempted to experiment again for a day or two.
But let's see what happens down the road. Maybe I'll miss the incidental documentation the typings provide.
Part of good language design is attempting to minimize bad programming habits. In this case, a language with a HM type system would avoid the issue all together, while affording the same productivity gains from having static types.
The places where the type system encourage rabbit holing are almost always where bad types/APIs are already prevalent. Where you’re starting at the type level and don’t have an inclination to allow all manner of dynamic nonsense it’s not that different from types in an ML-family language.
It took some getting used to because things end up a bit less obvious, but I found that with the IDE's syntax highlighting capabilities, actually inspecting the inferred types isn't as much of an issue. And then you spend less time typing out type names.
Always tempted to experiment again for a day or two.
Maybe that's it. I've never felt bad about settling for 95% of the benefits of static typing and slapping any/unknown on the edge cases.
With TS its like you’re writing the unit test and your code at the same time, and if your types are expressed well enough you can also skip the compile+run+verify step.
I’ve had cases where I would spend days just writing code, refactoring and iterating, without building the project once, compile it at the end, and have a complex feature work correctly on first try, passing all the functional tests.
It’s like I have a pair working with me constantly compiling my code and pointing out syntax and interface errors while I concentrate on the business logic and big picture stuff.
When I go back to JS it’s as if I now have to do all that manual labour too, as well as constantly write and run unit tests myself to make sure what I write actually works.
Though it definitely took some time to get this comfortable with the TS type system.
In code consuming types I relie on inference 80% of the time, sometimes I need to specify generics, if it fails I do dirty casts, if that fails I do dynamic blocks.
I found it far more productive that going through rabbit hole of TS and its compiler. I write my `definitions.d.ts` for my objects, and use them simply
/** @type {ns.MyType} identifier */
const identifier...
and voila, my IDE gives me auto-completes and warns me when I'm doing silly stuff.It works for functions as well using
/** @param {type} parameter name */
and is generally a good idea to use.It gets you 80% of the way there with no compiler/transpiler (although one might argue that the IDE is compiling non-stop on file saves, with its language server).
I embraced Typescript when it was new and the Javascript standard was a total mess. It was leaps and bounds ahead and closer to something like AS3. At the time TS was godsend.
Today, JS + webpack gets you really far using the latest ECMAscript standards.
Same with IDE. The amount of stuff my IDE can deduct based on my few typings and my use of modern strict javascript has strongly reduced the need for another transpiler (on top of webpack).
I found jsDoc to be a perfect 80-20 solution.
Because at the end of the day, TS isn't a compiler, it's a transpiler. You're not compiling to bytecode in a vm, you're still in javascript.
This saves my bacon in the front-end. For the back-end, I most definitely choose a strongly typed language like Go.
https://blog.isquaredsoftware.com/2019/11/blogged-answers-le...
I also try not to let any escape function boundaries.
Recast it to something better when returning something.
No it doesn’t. You’re casting to `any` and hiding that fact. Both top types can be anything at all, `unknown` is only safer if you narrow it to something else by testing it. If you cast it to something else you’re just treating `unknown` as `any`.
But yeah, sometimes you hit a wall and need to do something unsafe.
Probably just means you are smart and good at testing at some level ;-)
For me the biggest benefit of TypeScript is probably when I debug or extend other peoples code (i.e. all the time) and I don't have to hunt down calling functions to see what gets passed in.
I mostly work on large projects written by other people over the course of years and often this simple thing can save me several minutes several times a day.
If we couldn't do something 100% type-safe then we weren't supposed to do it at all. I kid you not.
I got off of that project as soon as I could manage without burning a bridge. Funny thing was that before that? I thought I was quite the stickler for getting types as correct as possible. Turns out I was off by an order of magnitude of what a real "stickler" for correct types could be...
The only downside of burning bridge is that now, I don't have an overview on how bad shape this project is.
Thankfully, the impulse to waste time trying to come up with the most precise typing possible for everything hasn't been as strong with TypeScript as it used to be with Haskell for me. In part that's because TS's type system is so ridiculously expressive that I can say what I want to say without spending too long on it anyway, in part it's because the system's proud unsoundness and the ability for typings to simply be wrong means that I know not to stake my life on the types anyway. Besides, in my experience, the more precise I try to make the types at an interface, the more I need to cast in the internals. Better to find a balance that keeps both reasonable.
Isn't that exactly why you use types to begin with?
But someone calling themselves startup-cto.net should be a lot more pragmatic about the value of strong types and willing to embrace the possibility of change!
Writing types give me more pleasure than it should... but if something is hard to type and unlikely to be referenced outside the local scope, it often isn't worth the effort.
My team was really happy with the result, but I'm afraid that if layoffs happen, I'll be on the shortlist.
I feel in that case you should have some type union of `"notAsked" | "none" | { t: "some", v: "Name" }`, because that would be more clear of what each possible value means. Let `null` and `undefined` be synonyms for `nil`.
You'll need to write undefined as a potential type for potentially undefined object members, and as such it means exactly that - undefined.
For everything else there is null.
A sanity check would be - undefined can only appear in a function parameter type or object member type as a union with other types, or in a === comparison. For everything else there is null.
Explicitly defining something as `undefined` is paradoxical and IMO should never be done. I've tried to explain this concept to some co-workers without much lasting success, but it's a pretty common concept to other languages.
Undefined should basically never be used as a value directly.
map.get("missingKey") returns undefined.
The new ?. operator returns undefined, even if you pass it null.
Undefined means that you don't have to care about the difference between { foo: undefined } and { }.
The only pain point is that JSON.parse() produces null, but this is relatively easy to work around.
I don't find any reason to ever produce a null. I just treat it as a value that can occur when using certain APIs.
If your code depends on the difference between a missing key and a missing value, probably you should be using a Map which makes that difference explicit with separate set() and delete() methods.
I actually created a `type Optional<T> = T | null` for one of my projects because I wanted a good way to describe something that is null vs undefined. This is specially important if you are de/serialising objects. In my case, undefined = user did nothing to it, null = user actually chose not to supply value (and yes, they are different)
Sorry if I am misunderstanding your statement.
T | null signifies that it can be cleared by using setState({prop: null}).
T | undefined means that it may start out undefined, but if it becomes defined it'll probably stay that way. If you try doing setState({prop: undefined}), it won't work.
const x = {};
x.foo // returns undefined
const y = { foo: undefined };
y.foo // also undefined
Basically a member not being provided will return undefined, whereas `null` had to actually be set. Hence I think the tendency for checking for the absence of something via `undefined`, while expressing the deletion of something with `null`. Sure, you can assert more specific types in TypeScript to step around that, or you can do something less intuitive like checking for `keyof` to detect absence; but this is both straightforward and presents less issues when interfacing with inputs from non-typescript-using users.
The semantic differences seem to be local at best. In any case, there's plenty of code that legitimately doesn't concern itself with any of that.
I've never ever run into this because I never use undefined for anything other than stuff that's explicitly not set, and I have no fucking idea why other people are. But I can definitely see it causing bugs if you do stuff like use '!=' everywhere and treat null and undefined as the same thing.
This is why the original comment is wrong. You should be narrowing your types where possible. It costs nothing to just always use null if you're explicitly setting something, then you can use '!==' wherever you want, which means that if someone else sets something to undefined you'll catch the bug straight away when it's easiest to catch.
If you treat them the same you're just asking for some 3rd party lib to do a '!== null' or accidentally overwrite a "null" with a default parameter you're not expecting, and that's way, way harder to troubleshoot.
You could just as easily run into a bug with 3rd party lib code by distinguishing between the two when that library doesn't.
The only reason I've ever intentionally used `null` in my own code is that it precedes `undefined` in the default array sort. But if I ever found myself using library code that required `null` for something, I'd say "Huh. That's weird." And then I'd pass it a `null`.
What? That's not what prevents you from running into bugs. The bugs come when you treat null and undefined as equivalent and try to pass them into a function with default params, without knowing which one you have. Because the defined behaviour of default params is different for null and undefined.
For example, say you have a data type of 'null or [0...11]' for a month input the user may or may not have interacted with yet. It gets passed around a bit and ends up going through a function defined with default params, like 'function doSomethingWithDate(day = 0, month = 0, year = 1900)', and in that function there's a null check like 'if day, month or year is null, do whatever we want to do if the user hasn't entered a date yet'.
A few months later someone picks up a ticket to refactor the date input, and they use 'undefined or [0...11]' for the month instead, which doesn't matter because your team always just checks both with things like '==', '!=' or '??'.
Except surprise, it does matter, because now something somewhere else breaks because it thinks the state is 'the user has entered 1/1/1900' instead of 'the user hasn't entered anything yet'.
Of course you might not run into this specific bug, because the code is a bit contrived, and most good programmers will recognise that whatever 'doSomethingWithDate' is it probably doesn't need to be run unless the user has actually entered a date already. That null check should be happening as close to the user input as possible, and there shouldn't really be any room for people to use default params between those two places.
The problem is that in the real world people write dogshit code like this all the fucking time. That would rate at about the 5th percentile of all the garbage code I've seen in my career.
My point is that that's literally impossible to run afoul of language differences between null/undefined if you're strictly using either null or undefined, as opposed to this blase mindset of "just always check for either", because you now know what your data type is while you're writing the code, and can look up the standard library or 3rd party library docs to see how they interpret it. If you have data types of 'null or undefined or ...' then you don't know which one you've got until runtime and you need to account for both possibilities everywhere. That means coercing it into either specifically null or undefined every single time you want to pass it to a library function, your own codebase's functions that use default params, or any other place the language itself treats undefined and null in different ways.
Always use the minimal data type for representing whatever you need. If you don't then you're literally choosing to have ambiguity in your code base. It makes no sense.
Also this:
> You could just as easily run into a bug with 3rd party lib code by distinguishing between the two when that library doesn't.
...is nonsense. If the library doesn't distinguish between the two then by definition it cannot matter which one you pass in, since it's treating them the same.
null: the value exists but is not set.
undefined: the value does not exist.
JSON.stringify on an undefined property removes that property.
JSON.stringify on null keeps that property as null.
A codebase that does not distinguish between null and undefined is a less flexible codebase.
At the end of the day it would have been much nicer for there to only have been one nothing type but what’s done is done.
Element.{firstChild, nextSibling} being null means “I know what firstChild (or nextSibling) is, but this element doesn’t have one.”
It would be categorically wrong for things like Element.{firstChild, nextSibling} to be undefined.
I’ve seen null/undefined semantics play out; they more or less collapse under the slightest of deadline pressures or scaling of team sizes. There are infinite ways to splice the idea of “emptiness” or “lack of data” and any definition you provide (the definitions both you and OP provide give a lot of wiggle room) would be interpreted fifteen ways by fifteen developers, so much so that even changing hands just once will cause any null/undefined distinctions to sublimate in any codebase.
There are so many languages which do just fine with one null type; I don’t understand why JavaScript developers have to do these sorts of mental kinesthetics to justify the existence of two in JavaScript, especially given the more or less insane fact that `typeof null === "object"`. If you consistently use undefined or null exclusively internally, and accept both via the == null trick externally, you can pretty much avoid having to deal with null/undefined semantics entirely.
> There are so many languages which do just fine with one null type; […] two in JavaScript
I quibble over your claim here, because JavaScript doesn’t have two null types. undefined is not a null type. Rather, it’s JavaScript’s equivalent of what is in most dynamically-typed languages an exception, and a compilation error in most or all statically-typed languages. As a concrete example, where JavaScript produces undefined, Python raises AttributeError or KeyError. There are similarities to NaN as well—both come from a philosophy of trying to keep the code running even if you ask for something that isn’t defined. Yes, undefined then behaves a lot like null in some ways, just as NaN behaves like a number in some ways (though the two cases are not parallel, merely passingly similar). But then, false behaves like null in most of the same ways too (though not quite all—most notably, the `== null` comparison—but it’s well understood that a lot of JavaScript’s == comparisons are illogical and inconsistent, so this should not be considered significant to the design); yet no one claims false to be a third null value.
(This is a fundamental problem of nullable types which is solved by algebraic data types; in Rust, the equivalent method would return None for “no value found”, or Some(None) if it found a None value.)
var x = {}.foo;
var y = (function() {})();
These are contrived examples, but they represent things done by very reasonable code. There are all kinds of ways that `undefined` can get assigned into a variable. It's inevitable that those cases would have to be handled.Yes, you have to handle cases where things are undefined, but I'd be wary of handling them too dissimilarly to where things are null, because `undefined` was not designed to be "Null 2: Electric Boogaloo", it was designed to be "404 Item Not Found Exception" in the time before JS had real exceptions.
"Less flexible" != "Less correct" or even "less good".
It's true that these are subtly different concepts. It's arguable whether they are helpful distinctions for the long-term health of a codebase.
Except, you can still differentiate between them:
> ({ foo: undefined }).hasOwnProperty('foo')
true
> ({ }).hasOwnProperty('foo')
false
So now you have two ways of specifying a missing property, instead of one. null vs. undefined is a really annoying language misfeature to me, especially with some of the other weird behaviors that it has: > [].length
0
> [].map(v => 123)
[]
> [].map(v => 123).length
[]
> [undefined].length
1
> [undefined].map(v => 123)
[123]
> Array(1).length
1
> Array(1).map(v => 123)
[]
> Array(1).map(v => 123).length
1
If you need to remove a property from an object, just use delete obj.foo; instead of setting it to undefined. JSON.parse(JSON.stringify(obj)).hasOwnProperty('foo')
false
Thing is, other parts of the code might actually serialize and unserialize objects. If you rely on a prop being 'undefined' meaning "prop is unknown like the firstName", as OP suggested, you might be in for a surprise.Some of the rebuttals below seem to be saying "But I use them differently!" Bully for them, but 99% of the time when it's not set, you don't really care why it's not set because it's not defined yet or because it's been set to `null`.
And any time they assume it can only be null OR undefined but not either, there's a chance for a bug. Even if I "know" it can only ever be null and not undefined, it's safer to just say != null rather than !== null.
Treating them differently in a code base is a legit semantic, but testing with != null works no matter the intended semantics.
I see more room for error with being explicit as a default than generalizing as a default.
Of course, TypeScript will say that 'null' can't be passed to createNewMessagesResponse directly. However, data gets pushed around a code base a lot and might go through third party packages or come in from an API response and could very well be 'null'. I proactively don't check for 'undefined', because I can't really trust this to be 'undefined'. Furthermore, in the example OP provided, 'null' and 'undefined' should have the same result (no message is shown, because no number was passed to the function).
However, the type guard section seems a bit off to me. Is the suggestion to check each and every property of `Product` in `isProduct`? Seems a bit verbose.
I tend to use Axios, which uses generics to set the payload return type (obviously, some trust in your API responses is needed here):
axios.get<Product[]>(...);In our team we use io-ts to validate all of our endpoints, but simple type guards could achieve the same goal.
Checking for the existence of a key on an object is dirt cheap.
Even if you happen to have a case where it is expensive, there's absolutely nothing that says your typeguard has to take that approach.
I've seen some reasonably sane code that just checks that the 'Type' field of the object matches the expected value, and the 'Version' field is the right number. It's not going to catch all the possible errors there if someone breaks the api contract, but it's a lot better than a raw cast.
In most cases it's worth it to trade the small performance penalty for correctness, especially since the cost of fetching data is already dominated by the network and backend.
Tbh the bench here suggests the only non algo perf worth doing would be switching to rust with pre allocated memory for each request. By the time we are up to the scale where the sin le digit gains are worth it we'll have more than enough capability for going straight to the software end zone.
I also use axios, and I also take advantage of the generics you've shown above, but I acknowledge that I'm essentially just saying "Trust me" and doing a cast when I do that.
And I'll add - that exact style of code has been a source of bugs in our production codebase before. A dev will pick the wrong type for the axios generic, and if the api response happens to overlap on the used fields, no one notices. Then it blows up 6 months later when someone tries to access a field on the defined type that wasn't actually returned in the api response.
Very much this. That scenario will break if e.g. the backend API changes. This is a constant source of frustration for someone I know, whose backend team has a habit of not communicating their changes to the frontend team.
There's nothing TS can do to enforce that the actual type of the response matches what the type declaration says (hence the need for runtime checks in this case).
We use it to great effect ourselves, by generating types for axios.
https://github.com/ovotech/laminar/tree/main/packages/lamina...
Now granted, you’re now trusting the api writers with their contract, but if its another team in the org we’ve found it to be warranted.
I can see how the proposed solution could be "better", but I'd rather avoid the validation code and instead get the type right.
I think we need to have a discussion about the benefits of dynamically typed languages and why many people purposely choose them over statically typed ones.
In my experience, 95% of the time TS would not have helped anything. And the other 5% where it would have is not worth the constant work required to type the whole application. And sure you can use ts just a little bit... but everyone knows that organizationally the pressure then begins to use TS for the whole app.
Types are nice in some circumstances but are they really worth it? I feel like the answer is no for the majority of applications.
Also "don't use 'any'" - there are a lot of edge cases where this can be very difficult to avoid. Even the TS experts have failed to help me figure out how to remove some "any"s. I'm sure there is a correct way, but it just shouldn't be that difficult.
Which is kind of sad and against the origins of TS, which meant to supplement JS without bringing all that uncool into the school.
Sometimes I dig into source of a supposedly JS library and witness the author not just the type-checking, but OOPy interfaces, private/public, and I again feel the itch that this superset is a more refined embrace, extend, and extinguish.
Someone who talks intelligently about the strengths of dynamic languages is Rich Hickey. His talks challenge some of the ideas where static typing is superior to dynamic typing. He is arguably biased in his position but it makes for a refreshing contrast to the static types argument.
Clojure is a great example of the qualities required to make dynamic typing really powerful, the main one being the large library of functions that can operate over all collections. This reduces the amount of custom code you write for data munging, reducing surface area for bugs.
The complete counterpoint to that is Go with no utilities for doing common data transformations, forcing users to re-implement similar logic repeatedly. It may as well be statically typed at that stage to avoid mistakes from writing the same code over and over.
The more powerful statically typed languages with generics then fall somewhere in the middle where you don't have to repeat yourself as much.
I don't get the example for #3. I understand using "unknown" instead of "any", but both examples look effectively the same in terms of type safety.
I also don't get the example for #4. Side effects are the inherit "blind spot" of TypeScript. If you're really worried about your API changing on you, it seems like you'd be better off modeling it in something like JSON schema. Unless maybe your API is trivial.
What type guards are good for is marrying the compile time checks with runtime safety. They allow TSC to infer that in a given code path, the type can be narrowed down to something more specific. This is nice because it lets a single function e.g. handle a few different types without littering your code with `as` or `!`.
It’s easy enough to do this in userland. I wrote a tiny library called narrows which has worked great for me: https://www.npmjs.com/package/narrows
After I looked at it, I have a sour feeling in my mouth. Why there can't be a way (or a library) to do very easy type guards in TypeScript? Something as simple as the keyword `is` in C#
I’ll add that writing manual type guards is fairly infrequent, so in practice there’s not a lot to gain here.
*Modulo polyfilling newer ES features if you compile for a lower version of the spec.
That you can do something so sophisticated is a testament to TypeScript's type system.
However, if you were writing JavaScript, I think it's (unfortunately) uncommon to have this kind of proactive validation. So, for people writing TypeScript as "JavaScript + types" (which is a perfectly fine way to use it), they would just use a type assertion, which is just writing down the assumption that the JavaScript programmer makes when consuming but not validating external data.
https://2ality.com/2020/06/validating-data-typescript.html#p...
For #7, in a lot of (most?) cases, the name of a generic isn't meant to be descriptive. I'd argue that functions that expect it to be are doing it wrong. If I have a `function reverseArray<T>(arr: T[])`, the point of T is that it can be anything. Saying `reverseArray<ArrayTypeToBeReversed>` serves no purpose.
TypeScript also really, really needs to have a strict typechecking mode. Yes I know the "it compiles to raw JS" arguments, but it could easily add extra checks in the JS code (basically what example #4 shows).
Unknown is basically asking the dev to confirm the choice (ideally through a typeguard, but also with a simple cast).
And if you're using a typeguard, just define the argument as 'any' ex -
type AType = {
c: string
}
let a: unknown = {
c: 'test',
}
if (isAType(a)) {
console.log(a.c) // no type errors!
}
function isAType(a: any): a is AType {
return typeof a.c == 'string'
}`map<A,B>(fn:(el:A)=>B):B[]`
What would the alternative look like in what you wrote? In and Out? Range and Domain? Generic type variables are often too abstract to give a sensible name to.
This is the one "bad habit" that I think is a bit silly - naming conventions can vary by project.
Even in your example, I'd rather read this:
map<IN,OUT>(fn:(el:IN)=>OUT):OUT[]
Plus, it really is a habit. If you stop doing it for these kind of examples that really are very generic, maybe people wouldn't also use only one letter for cases where there is more of a difference between them.
`fork<A,B,C>(fn1:(el:Iterator<A>)=>B,fn2:(el:Iterator<A>)=>C)`
> Do you also use one letter variables and parameters?
A question in response to your question... Do you ever give an ALL CAPS name to a class? Why not?Maybe it's the set of conventions I'm used to, but I find T, U, V familiar and IN, OUT jarring and dramatically less readable at a glance.
"But do long names even help? I imagine, in such a project the system of logic can be so complex that a new dev cannot use them to their intent without completely understanding the code."
Meaningful names help a person gain an understanding of the code. And that's not even getting into whether or not "completely understanding the code" is a tractable goal in the first place (I don't think it is, even for the people who wrote the code, much less those who come later)
The actual research is that long variable names make it harder to understand code. It makes the variable name become an integral part of any code snippet that a reader feels compelled to memorize the specific names used, and that memorization results in errors and increased recall time.
Short variable names, including single letter names, allow a reader to basically ignore the name itself which allows them to accurately memorize an entire chunk of code without feeling the need to memorize long names. This improves recall time.
In simpler terms, when a snippet of code uses descriptive variable names your brain is inclined to believe that those names matter and that you need to remember them, compared to short variable names that your brain will not have to bother remembering and instead frees you up to focus on the structure and semantics of the code snippet.
The study that you can read, which is quite insightful is here:
https://pdf.sciencedirectassets.com/271600/1-s2.0-S016764230...
Note that long function names DO help, but long variable names do not.
That said, feel free to use long variable names if that's what you like and your project does it that way. Don't, however, express that as some kind of extensively studied software engineering principle that has research backing it. It's basically one of those things someone decided was true and everyone else just decided to believe it without ever doing the kind of work needed to validate the hypothesis.
In software engineering, we are still in the days of Aristotle, where we simply believe things to be true because we want them to be true. Just like the days of Aristotle I wouldn't be surprised if it takes us literally thousands of years before someone comes along and decides to question and test basic assumptions we all take for granted.
We can debate the value of one-word (or even acronym) vs five-word variable names, but the key factor in my view is that the name holds some sort of intrinsic meaning to the person looking at it, vs being an arbitrary symbol. For example, x and y - despite being one-letter variable names - are perfectly sensible in a context where you're talking about euclidean geometry and referring to values on the x and y axes. But they are not reasonable names in the context of business logic where they represent customers or transactions. Alternately, a software shop may establish a convention that generics A and B mean something specific across all contexts in their codebase. A company-specific convention this arbitrary will still increase the barrier for newcomers to comprehend the code, but once learned they will at least be able to re-use the knowledge and A and B will become meaningful to them. In this way, "meaningful" may be relative. Usually meaningful names will take the form of one or more words, but not always; I wouldn't say the actual length itself is what's important.
With that established: by choosing names that have intrinsic meaning to the reader, the reader's mind is allowed to skip a step of indirection where they would otherwise have to perform a mental "dictionary lookup" from symbols to concepts. To be frank: study or no study, it's literally unbelievable to me that removing this step wouldn't help the slightest bit with both comprehension and recall.
Addendum: There's a further dimension to this which is that code is more than what it does. Take the following function:
function m(a, b) {
if (a < b) {
return a;
} else {
return b;
}
}
Is this code wrong?Because the function lacks a meaningful name (or documentation), it's not simply hard to tell, it's a question with an undefined answer because the intent is not represented in any way. This function currently returns the minimum of the two arguments, but there's no indication whether it was meant to return the minimum, or the maximum, or even the sum for that matter.
Maybe you could argue that we should be writing comments anyway, but that still moves the meaning across a step of indirection, where you have to go find it instead of seeing it inline. We absolutely should be writing comments, but they don't completely replace good naming.
All I'm saying is that as professionals, there is a mismatch between how strongly we believe things to be true and how much time and effort we invest in validating those beliefs. People here will argue very strongly about long vs. short variable names and this discussion could go on and on and on, but no one here will actually try to find existing research or conduct any research of their own.
My position is until you, or me, or anyone wishes to hold a strong belief about what is truly beneficial in terms of productivity, human psychology, etc.. we should hold off on making claims about objective engineering facts and instead simply accept that our claims are stylistic in nature and used out of consistency and convention, rather than because we have evidence that it's superior.
Finally, it really is worth noting that it was literally unbelievable to Aristotle that heavy objects fall at the same speed as light objects, so much so that no one bothered to test that assumption for thousands of years.
If you, or anyone, is so convinced that some coding convention is more than just stylistic but actually objectively superior, why not put in some degree of effort to conduct a scientifically reliable test to validate your belief? If the answer is that it's too hard to put in the work to objectively validate your opinion, then you should also accept that it's too hard to accept your opinion as an objective fact.
If we as professionals are not willing to validate our beliefs in a rigorous manner, then we as professionals should not feel so compelled to proclaim those beliefs as being objectively true instead of simply a matter of preference.
And single character variable names don't require memorisation? raises eyebrow
Your example is clear enough because the signature is simple, but in more complex functions I've found that becomes obscure real fast, particularly if types are then passed down to something which also uses generic types. Enabling people to reason about things is not as clear as actually telling them what it is. If the 2nd of 3 generic types is the return type for whatever reason, call it SomethingReturn type and remove that ambiguity.
For generic that doesn't have any specific meaning we can use T.
Adding a meaning to meaningless generic will add confusion, like in the example, Element might conflict with HTMLElement while it can be something else entirely.
Seems like for "6. Optional properties"
One should rather use tagged unions (see: Abstract Data Types, variants) that are usually written like this in TS:
type Product =
| { type: 'digital', id: string, sizeInMb: number }
| { type: 'physical', id: string, weightInKg: number }
At least I find it more elegant, concise and fun to work with :)Or if brave enough to dive into the deep end of FP, io-ts is nice. https://github.com/gcanti/io-ts
Tagged unions might get a bit messy if you ever need to extend them, but sometimes you simply have some source / sink of data that requires a variant type.
2. The recommendation goes too far in suggesting fallbacks in the parameter list (and would if it had suggested the same for destructuring options/objects generally), without explaining that for some braindead reason fallbacks aren’t `null` safe.
3. This leads into #4 but if readers leave here they’ll think the cast from `unknown` is somehow safer. I have seen this happen, people just cargo cult stuff. I’d much rather a 9-length list than risk that.
5. This particular problem/example is spot on but I’d like to expand the “in tests” scope to explicitly explain why some unsafe types are justified in tests. I always specifically unlint rules about non-null assertions in test because a non-null assertion is an additional safety mechanism in that context: it validates that the runtime agrees with the type system. If you intentionally but explicitly bypass the type system and don’t see an error you should know that’s a bug and fix it.
7. Is mostly spot on but conditional. There are unfortunately a lot of conventions at play. Anytime there’s ambiguity definitely add verbosity. If your type is truly generic without constraint adding that verbosity is actually harmful.
10. This is just plain wrong for the vast majority of cases. It’s also a JavaScript-ism that doesn’t translate well to other languages and the “bad” habit helps avoid mistakes. For most use cases `null` should only have one meaning: absence of a value. The cases where it doesn’t are almost always a mistake (oops this was an “object”), and the cases where the distinction does matter are usually (but not always) a sign that your interface is bad (the exception being explicitly passing `null` sentinel values to overwrite existing data in a stateful external system like a database). This should be the exception not the rule and making it syntactically clear is useful.
TypeScript will believe what you tell it in a type guard's signature without looking at the body of the function further than "does it return a boolean".
I don't quite understand the criticism. You may as well say that unit tests are just as unsafe as not writing unit tests, because your test might not do what you think it's doing.
I elaborate on ways to use them effectively in the post.
Type guards have a runtime overhead, don't they?
(You could argue: "If TS can not infer the type at this point so neither can you. Better to check at runtime." But from my experience, sometimes TS is simply not smart enough.)
(I can think offhand of one case in the last year where I've wanted to use a type guard for a reason that didn't have to do with runtime validation, anyway. Even then, it wasn't the solution I ended up going with.)
In most contexts, the reasonable approach would be to only do runtime validation at your system boundaries and not anywhere inbetween.
Thinking about how to solve this, it occurred to me to use a type guard, since it would have effectively convinced the type checker that my object instance was in fact an instance of the class of which it is an instance. But it would've been a wart and also imposed the aforementioned runtime overhead - not that that would've been meaningful in this case, it wasn't in a hot loop or anything, but I still didn't like it. So I ended up instead exporting the shared dependency from the module and having the consumer import it from there, rather than from its own (IIRC otherwise unused and thus eliminated) copy of the shared dependency.
Does JS need a better way to introduce json type safety? C# and graphql use schemas, I wonder if this is a better approach (compile time, not run time).
But yes: I think TS is missing a way to derive runtime functionality from the type definitions - at least in the cases, where the types describe plain JSON.
Something along the lines of clojures' `spec`.
type Spec<T> = T extends (x: unknown) => x is infer U ? U : never;
type Foo = Spec<typeof someTypeGuard>;Otherwise this is a great list!
Unfortunately I feel like every language design team prefers to learn their mistakes on their own instead of learning from others (with the usual “our language is different” excuse).
Then code should use != null comparisons which match the none types semantics without needing runtime code generation.
I think this would violate another typescript principles that existing libraries and patterns should be expressable in typescript.
Some schmuck wrote a library that accepts null but not undefined so typescript has to distinguish them.
With codec, one can make sure that the type is correct and defining type and type-guard becomes a one-time task.
> This habit grew I guess because even the official docs use one-letter names. It is also quicker to type and requires less thinking to press T instead of writing a full name.
This habit grew because every language with generics uses such short generic type names and frankly, they are easier to read than MyFancyGenericTypeName.
Thanks for the rest of the article. Pretty good read.
Disagree. Also this same logic can be applied to every other token (ie this argument suggests we should write minified code).
function head<T> (arr: T[]): T | undefined { return arr[0] }
function head<Element> (arr: Element[]): Element | undefined { return arr[0] }
No, it does not. Think of the type variable as a template. The value of this kind of variable does not change during the execution of a function, once given, stays the same. When reading code with generics it does not even make sense to compare the names across two different functions unless they are methods in a class.
Basically, a T in head<T> may be something else than T in tail<T> depending on what the container declares. And what if I use your head<Element> as my head<Leaf>. For me semantically it is not an Element so your forced generic naming is simply going to mess with my brain logic. Leave my T alone. My T is my Type, not some imaginary thing the person who wrote the API imagined I should be putting in there.
This new logic can be applied to every other constant.
> When reading code with generics it does not even make sense to compare the names across two different functions unless they are methods in a class.
Agreed. But I don't think anyone was discussing that.
Constant is by definition constant. Generic type is known and bound only in the given code path and may differ across two different executions. That's not constant.
I find it excellent and definitely a helpful tool to think about when you are struggling with type safety vs runtime data.
I don't think I'll ever be able to convert to using typescript, I'd rather have validators running in the code checking external inputs for the proper fields at runtime before the rest of the system blithely processes whatever it gets due to safe assumptions. Add integration test for specific modules and everything works fine.
I don't want to compile languages that don't need compilation and I don't like the fact that I need to create all of these fake feeling types which are actually just objects rather than javascript classes. It's just feels like boilerplate when you could instead include the 'type' that it's supposed to be in the name and look up a validator for that 'type'.
I'm probably wrong, but it really doesn't feel like it.
It doesn’t obviate the need for dynamic validation nor is it meant to do that. But it does give you a clear point of where to perform those validations rather than littering it everywhere throughout your application.
It doesn’t obviate the need for testing either. But it does eliminate an entire class of dumb programming errors such as incoming a function with the wrong arguments.
TypeScript alone doesn't make any assertions and doesn't offer any checking at system boundaries ("external inputs"). That's not what it is for, and the types you specify with it aren't objects. All that stuff gets compiled out before the code is even run. The point of TypeScript is so that you don't have to run your code to know whether the way it handles data internally is the way you intend it to.
Validation at system boundaries is a separate concern, and handled separately. I like to do it with a package called "runtypes", which both supports arbitrarily complex shapes in its validators, and expresses them in a way that's very close to TS and can be trivially converted into compile-time TS types so that you don't have to write the same types twice.
I wasn't sure what to make of TypeScript for a while, too. But having once tried it, and since become very conversant with it, I'll go back only for trivial tasks or under significant duress. Sure, writing types is a fair bit more work up front and a little more ongoing, but it really does make a huge difference in maintainability; in my experience, it's a little work now to save a lot of work later, as the resulting code is both less likely to exhibit an entire class of often quite subtle bugs, and easier to reason about and modify.
As you've mentioned, integration tests are important, and cover your assumptions on a closer-to-the-user level.
I suspect that there's a flock that prefers working with the testing pyramid remaining standing. Put another way, are uncomfortable if the lower bases are unstable.
There is another flock that prefers the agility and flexibility that comes without logical gymnastics of type checking, relying on inverting the pyramid to prove they're done. Not right or wrong, but another way of making.
Well it's a transpilation, not a compilation, and you are able to run .ts files in dev mode without transpilation.
"fact that I need to create all of these fake feeling types"
I feel the contrary to you. I want everything to be well defined in my application, especially if it's a server :)
Untyped exception in TypeScript is one of the most vulnerable point of static type escapes. Unlike any, which is explicit, exception caught in catch block is implicit.
Personally I prefer to wrap catches into a specific type/symbol `UnknownError { data: unknown }`, and discriminated union to indicate success/failure on return. fp-ts (https://github.com/gcanti/fp-ts) is my go-to library for this
from datetime import datetime
import time
print(datetime.utcnow())
def show_ts(d=datetime.utcnow()):
print(d)
time.sleep(5)
show_ts()
where as in TS, the 2nd one will be roughly 5s older: const now = Date.now();
console.log(now);
function logTs(d=Date.now()) {
console.log(d);
}
setTimeout(()=> {
logTs();
}, 5000); function example(a = 1) {
...
}
behaves the same as function example(a) {
a = (typeof a !== 'undefined') ? a : 1
...
} function test(a = 1) {
console.log(a)
}
test() // -> 1 is printed
test(null) // -> null is printed
test(undefined) // -> 1 is printedYou could have sugar evaluated once, sugar evaluated each time, not-sugar evaluated once, or not-sugar evaluated each time.
If you can find ways to avoid the escape hatches you can then get the compiler to carry more weight and trust in the types without blowback.
edit: read too fast :facepalm:
Anyway great blog post
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
bangbang for life
My pleas, of "just fold the imports in your editor" and "it makes the types globals, so it's not modular" and "interface /declaration merging becomes an issue" all fell on deaf ears.
Some people like their bad habits because it makes sense to them, I guess.
The only one I know of is tslint:
There's a cost to type coupling and over specification