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.
I think of “T” as the template parameter the same way I think of “i” as a loop iterator. It’s a conventional shorthand, so if it’s contextually clear, like if T is the only template parameter and there aren’t complex constraints on how T behaves, there isn’t much harm in using it for succinctness.
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
Thanks for providing this feedback - now I'm glad I actually bothered to write it all out, because while I was doing that I was thinking to myself "This is going to be nonsense to anyone who doesn't already understand the first one".
On the contrary as someone who moves somewhat comfortably between Java, C# and TypeScript projects the tendency to use especially I in front of interfaces but also other letters in front of other things are an explicit pain point.
(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. :)
Even without its basis in the history of COM, the I-prefix for interfaces types in .NET makes sense because interfaces-types are very structurally and semantically different to "a type's interface" - so having some way to quickly distinguish them at-a-glance is necessary.
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.
I too enjoy C# a lot and find Typescript close to perfect (obviously it is constrained by its relation to Javascript past, present and future).
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.
> Powerful IDEs can tell you just fine what is a class and what is an interface.
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.)
> Forget I<Whatever>. It breaks sorting and it is allows people to avoid thinking if the interface is necessary or not.
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.
ThingImpl is better for searching and has the advantage that Thing/ThingImpl will be next to each other in an alphabetical list of your classes. It also means using the interface is more natural than using the implementation, which is usually what you want.
Another common convention is to select a suitable letter: for types with no particular meaning, go T, U, V, &c.; for types with meaning, see if there’s a better letter, e.g. Map<K, V> for a key-value map.
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).
It seems most coders agree that variables, functions, files, columns, classes, etc. should have unambiguous, explicit, names. Writing `m[k] = v;` would not pass many code reviews. I don't see what makes generic types any different.
Context matters too though —
As a general rule "i" is a terrible name for a variable but in a simple unnested loop it's expected.
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.
Agreed. I think naming generic types makes the most sense with multiple types, or when more specific names make sense. For example, if a generic argument must inherit from a certain type: `class SomeView<ViewState extends BaseViewState>` or some such.
`Element` is as non-descriptive as `T` in the author’s example and, IMO, doesn’t add any tangible benefit.
I use a simple rule: if I can understand what it does in a code review, I won't call it out. If I don't, then the name is probably too terse. Most of the time, T is just fine.
I write C++ for a living, and one thing I've kind of noticed a similar thing to what you've mentioned. "very generic" template code often is best left to a name such as T, but a very specialized piece of metaprogramming often benefits from specific naming.
The latter scenario being that hopefully the person debugging your logic a decade from now has some insight into what you were thinking, namely.
Some codebases prefer `TItem` - the preceding T signifies that this is a type variable, not a type, which is quite helpful when reading type signatures.
> That's one thing I like about T, it's very self evident that it's just 'the' generic type.
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.
My side-project convention is all-caps, e.g. ELEMENT. No underscores. Always one word. It looks like a generic parameter and its name conveys meaning. It really helps when there are multiole generic parameters.
11. Going down the rabbit hole spending three whole days getting your typings perfect for some weird use case, instead of writing actual code. Sometimes there's a benefit down the line to having done it, often not, and knowing the difference is where greatness lies.
This is definitely something that typescript constantly lures you into.
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.
I used to think this way too, but working over the years with TypeScript has gotten me so accustomed to it that it’s now the other way around. I feel much more productive in TS in general.
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.
Perhaps much like the parent, I want to create without the foundation of TDD or unit tests, so I find the mixture to be expensive in the cases I'm typically working.
One of the things I learned, not from TypeScript but from other languages with type systems that work similarly, is to minimize the use of type decorations and let type inference do most of the heavy lifting.
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.
Huh. I recently built a language parser in TypeScript and it was a very positive experience. As just one example, discriminated unions for AST nodes worked great.
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.
For my company production code I definitely spend way too much trying to implement the correct types, however for side projects and MVPs I do quickly what I have already learned or just sometimes if it's too complicated I'll just go with "any" styled solutions or similar things. I think using TypeScript flexibly for prototypes helps me do that "first compile test" will work out of the box very easily.
This isn't a fault of TypeScript, but any language. Trying to predict the future, trying to predict and eliminate technical debt, in a rapidly developing product.
But it is the fault of TypeScript for having such an obvious path for falling into that trap. Part of what it means to have TypeScript code is to have typing files.
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.
This isn’t actually a fault of TS, it’s an explicit goal. They design the type system not to guide usage but to make existing usage explicit and trustworthy.
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.
Don't treat types as theorem provers, treat them as formalized docs - getting dirty with dynamic under the hood is perfectly acceptable to me (say you're doing some metaprogramming - typing that out is usually a lot of effort for low gain) - the types just help define intended use.
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.
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.
Absolutely. One of my strongest opinions about TS is that you should use it pragmatically. It's not worth burning hours of time trying to placate the TS compiler if you know the code works okay - throwing in a `type $FixTypeLater = any` and moving on is an acceptable workaround depending on the situation:
> “as unknown as MyType” usually works almost as well and avoids throwing you out into the cold untyped darkness.
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`.
On the other hand, most of the time I'm willing to put in a little effort to express the desired types, since it dramatically improves the intellisense suggestions, making code edits / refactors much less of a mental burden.
But yeah, sometimes you hit a wall and need to do something unsafe.
I was on a project-from-hell once where the project lead cared far more about getting types perfect than allowing the code to be, you know, useful.
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...
Same story here, on top of that, the tech lead had the most aggressive linting on the planet (no ES class allowed, limited arrow functions, and so on....)
I burned bridge, I don't want to have to deal with people that mind fucked limited in their brain where only good code pass this abusive code lint.
TS is really cool, forcing the use of `any` is really dumb.
The only downside of burning bridge is that now, I don't have an overview on how bad shape this project is.
This has been my experience. I've used it on a handful of projects over the last few years and while there have been some benefits it has definitely decreased productivity. I can't say that the TypeScript that I've written has been any more bug-free that the regular JavaScript I've written (or my team mates).
> I can't say that the TypeScript that I've written has been any more bug-free that the regular JavaScript I've written (or my team mates).
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.
Yeah, that's definitely handy, or the ability to right click on something and click "Go to definition" to jump right to it. I'm not saying it's worthless, but all of the legwork to get that to work isn't worth it to me.
I kind of came to the same conclusion at some point. I normally find statically typed languages much more productive. If you’re working with all typescript libraries with great typings it’s probably great but the reality of the javascript ecosystem means you’re often wasting time getting the compiler to understand how a few dependencies should cooperate. That and the fact that I contracted on a few larger projects that were in a transition phase filled with ‘any’ which often meant little type safety but a lot of extra typing.
I have software I've personally written that's still running 20 years later. Most of my JavaScript/TypeScript is now many years old. Getting your typings perfect for some weird use case might not be useful but 3 days -- that's nothing.
In the limited TypeScript I've done (only a few months so far), every single time I thought "oh well, let's leave this at any for now" or "ah, crap, ts-ignore until I can think of a better way" or even "let's disable strict-null-checks for now, as fixing all those cases is too much work" has resulted in broken code down the line because assumptions that are not caught by the types have been invalidated. So I tend to err on more or better types for now. Some things can't really be improved of course, but that's where experience comes in over time in recognizing those cases.
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
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.
A while ago I spent a week turning a humongous interface with all fields optional into a discriminated union of smaller ones, falling back to an "any" dictionary.
My team was really happy with the result, but I'm afraid that if layoffs happen, I'll be on the shortlist.
I love going down rabbit holes chasing perfect type definitions.
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.
I think it's important to remember that TypeScripts types are there to deal with the insanity of the JavaScript ecosystem, and are not there to be Haskell.
I agree with most of these, but I pretty strongly disagree with #10 (don't use `!= null`). The reasoning is that you can use `null` and `undefined` to mean different things, like `null` being "no first name" and `undefined` meaning "haven't asked yet". I agree that strict TS lets you do stuff like that fairly easily, but I think this is a really bad idea, because there's nothing that would indicate to a reader of that code the meaning of `null` vs `undefined` for those properties.
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`.
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)
This why I don't fully agree with #2. I often avoid default assignment because it only works for undefined and not null (or was it the other way around?) which can be surprising and cause hard to find bugs. || allow falling back from either. Should be used with some care though. I used to use || more frivolously.
I strongly advocate for using both BUT not propagating undefined. That is to say, use null and handle undefined.
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.
The way I always see it is that `null` is for known unknowns, and `undefined` is for unknown unknowns.
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.
It's funny, I have a policy of the opposite. Use undefined, don't propagate null.
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.
Disagree on #10. The amount of code I've seen that intentionally distinguishes between null and undefined is... well, zero. On the other hand, the amount of code I've seen that unintentionally distinguishes between them when it shouldn't have is not zero.
Trying to finely parse the semantics of null and undefined like this is a fool’s errand, insofar as there isn’t much in the language which enforces your conventions. Also, these conventions fall apart when we check how null is actually used. For instance, all DOM nodes have a default of null for their first children and next siblings. Does this mean “the value exists but is not set?” What does it mean when the browser is setting the value and not the user?
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.
Some developers would interpret your rule as being, if it doesn’t make sense for you to make this property access on this specific object, the value should resolve to undefined and not null. Problem is, there already are mechanics for a kind of check like you describe (the in operator and hasOwnProperty both kinda do what you’re looking for). Moreover, if you were to design the DOM API from scratch, by your logic some developers might assume that text nodes, which never have children, should have a firstChild set to undefined and not null, because it should say “I don’t know what you’re talking about.” You can debate this last point, but know that some people are going to interpret the null/undefined rules as you describe in this way.
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.
The early DOM APIs certainly have some warts due to inheritance design mismatch, because they designed things in a single-inheritance way despite DOM actually being multiple-inheritancey. These days, new things like the append() method get added to the ParentNode interface, so that they’re provided by Element and DocumentFragment, but not such as Text or Comment; but back when they designed these things at the start, they only had Node, and decided to implement these methods on Node rather than individually on Element and DocumentFragment. While I imagine there were practical reasons they did it that way then, with the benefit of hindsight I state categorically that this was a mistake and is a design error. If the spec- and browser-makers were willing to break backwards compatibility, firstChild would certainly be shifted from Node to ParentNode, and text nodes would no longer have a firstChild member.
> 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.
Why does Array.prototype.find() return undefined and not null if it didn't find a value? According to your logic, the sentinel to indicate "this array doesn't have an element that matches the predicate" should be null.
I don’t know. I would guess that that return type involved a slightly heated discussion before undefined won over null so that you can distinguish it returning an element that is null.
(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.)
Also, the "wisdom of the Ancients" in JS was to never intentionally set a value to `undefined`. While a lot of the quirks have been paved over by TC39, `undefined` was originally considered an implementation detail of the JS engine and had different semantics in different engines, especially when trying to set something to `undefined` (`myobject.field = undefined` might be equivalent to `delete myobject.field` in one engine, equivalent to setting to `null` in another engine, and its own explicit primitive value in a third, while a fourth threw an error when setting anything explicitly to undefined because it was not a value at all). Even with paved over semantics and a general convergence among JS engines (and a near monopoly of V8 in practical usage), I still find it worrisome seeing codebases that treat undefined and null as very distinct values instead of shades of gray of the same concept, because `undefined` certainly wasn't meant to be a value originally.
There has been no version of javascript where it was practical to avoid setting a variable to null. As far as I know, things like these would always do it.
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.
I'm saying it impractical to consider `null` and `undefined` as different "values" in JS. `undefined` was originally built to be a "thrown" exception built for a language without thrown exceptions. It wasn't intended to be a placeholder value like null is. The language today makes it possible, you can today write `var x = undefined` and expect things not to blow up or behave all that differently/quirkily between browsers and browser modes. I'm still going to be worried/skeptical of any code that intentionally uses patterns like that of setting objects and properties explicitly to `undefined` and treats that as a different value from `null`.
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.
A better example is querying from a database or api where you can specify the returned properties. null === queried but no value vs undefined === not queried
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:
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.
Hmm, not sure what you're saying exactly. Null and undefined are different. A common example would be any sort of patch operation - setting a value to null is valid, whereas if the value of that property is undefined, it likely should be ignored.
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.
There doesn't seem to be any fundamental reason why that had to be the case. I'm not a react expert, but it seems like it would have been more obvious to me if null and undefined were treated the same. The distinction should be whether the key exists in the object.
It's a poor design carried over from legacy JavaScript, but there are clear differences at the language level. While not the only one, the biggest one to me is this kind of code:
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.
I think what he’s saying is not that there is no semantic difference between the two, but that in practice programmers use the two so interchangeably that not checking for both is likely to cause more trouble than it’s worth.
Yes plz. No one wants to know the difference between those two. Of course, you still do need to. But don't make the problem worse by writing code breaks when you mistake one for the other.
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.
Welcome to any of my codebases. Undefined and null have clear semantic differences, and those differences are important to the function, safety, and performance of the code.
That's perfectly fine. If you need to distinguish between them, then you should do that. There's plenty of code that's different though. I would say probably most of it.
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.
'work' as in if you explicitly pass in undefined as a parameter to a function, the default parameter will be used in it's stead.
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.
I've never run into this, because I never explicitly pass an expression when I want the default parameter value to be used.
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`.
> I've never run into this, because I never explicitly pass an expression when I want the default parameter value to be used.
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.
Yes, I disagree on #10 as well. Because:
createNewMessagesResponse(null)
'You have null new messages'
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).
I like the "why we do it" part in every section. Kudos to the author for that.
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):
To me - A type guard lets me be as picky as I'd like to be in the given circumstance.
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.
If the api has some contract with it OpenApi / Swagger / etc, its surprisingly easy to write a parser that would convert those to typescript types. TS has an awesome use as a library itself where you can write the ast with, and then tell it to convert it to code.
We use it to great effect ourselves, by generating types for axios.
> And I'll add - that exact style of code has been a source of bugs in our production codebase before.
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).
The word in the back of my head wasn't Verbose. It was `Expensive`. Explicit looping and checking values like that seems like the answer to 'why is our app slow'.
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.
You'd only do this once, at the point data is going into your app via e.g. a network request. Data from the network is an untyped blob, and all your TypeScript efforts fall apart if that blob doesn't match your expectations (which are encoded in your types).
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.
Indeed. I have recently been benching an ingestion pipeline (minor data mangling, then chuck into the db), and considered switching the exhaustive type checking off, or at least making it probabilistic. However profiling showed that less than 3% of cpu was spent on this validation, making it worth every cycle.
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 believe this to be entirely sufficient as well, despite not being truly safe. It's either going to throw an error immediately or on first bad access. It makes no difference to me.
I can see how the proposed solution could be "better", but I'd rather avoid the validation code and instead get the type right.
Reading through this list reminded me why I actually don’t want to use TS in the first place.
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.
When enforced like a code coverage score I agree. If in your day-to-day there's static checking against any/unknown, well, that's effectively saying 'only type-checked code welcome'.
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.
I totally agree. I thought typescript might grow on me as I got to grips with it, but alas a year of using it and it's still slowing me down a lot for how much it's helping.
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.
I'm kind of resigned to it. There's places where I imagine I'll end up using it and others where I'll do without.
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 disagree. I really don't find Typescript hard to use at all, and I've preferred the security and well defined variables and functions that Typescript provides. I would use TS over JS any day.
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.
I also don't understand 4. This looks terrible. Why use TypeScript if we have to make very manual assertions about the structure? Isn't that the purpose of TS itself?
TypeScript's type checking is static, so on its own it can't know that values it receives from elsewhere (like JSON coming from a string) is the correct type. The only option is for the programmer to check at runtime, which allows TypeScript to know the type along the code path in which the check succeeds.
This makes sense; but it still doesn't feel like the right answer. Facilities should exist for runtime type checking based on TS definitions, and syntactic features should enable a 'hard check' of the actual object when necessary
When I read your comment I had high expectations about 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#
It’s for the reason I mentioned before: one of TypeScript’s primary goals is that it generates no runtime code. If you run the TypeScript compiler, it will generate exactly the same JavaScript code with the types removed*. Since there is no such JavaScript feature, there will be no such TypeScript feature.
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.
There are libraries that do type-safe schema validation (an article I found in a quick Google is linked below).
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.
In the fixed version of #3, the function return type is `Product[]` instead of `any`. The example is further refined in #4.
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 `!`.
Soft disagree with 3. unknown might be better than any but then the typechecker isn't going to let you read any fields from the object, even if you are using a null check. And the given example is weird because you don't need to add a type annotation there at all. The problem really is that TypeScript doesn't have an "unknown deserialized JSON" type.
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).
Disagree with you on 3. 'unknown' is handy to prevent 'any's from leaking into the codebase. It's very easy to accidentally assign an 'any' to another variable and then return it, or to accidentally read a property off of it before ensuring it exists.
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'
}
I disagree with claim that One letter generics are a bad habit. In my opinion, it is proper to use one letter generics but start with A then B, then C... so it enables to reason about generics positionally.
Pseudo example of this:
I wouldn't agree they're a bad habit either, but only when used with care. Types like T, and K, V might be as clear to most people as i and j in array operations.
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.
I think I agree with you (though I would favor T, U, V as the sequence).
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.
Hard disagree. Having come into a TS codebase from the outside that made liberal use of generics, they can make things incredibly difficult to follow if they don't have really good naming and/or docs.
Yes. But do long names even help? I imagine, in such a project the system of generics can be so complex that a new dev cannot use them to their intent without completely understanding the code.
You could say the same thing about variable names:
"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)
Long variable names don't help either. It's been mostly a stylistic choice and a recent trend that stylistically long variable names are nice. But there are plenty of projects that use short variable names or even one letter names, and I don't see any evidence that those projects are less productive because of it.
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:
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.
By all means, use long or short or whatever consistent naming convention you'd like, even with the study I provided, the difference is not going to make or break any project.
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.
Do you also use one letter variables and parameters? If not, what is different about generics?
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.
Sometimes I do, sometimes I don't. If it's an iterator I often use one letter names such as i, j, k. Similarly if it's a generic unbounded type I also give it a one letter name.
It might be 'jarring' but looking over the original line I didn't parse what those types were doing at all, and T/U/V wouldn't be any better, while IN/OUT I can understand without explicitly thinking about it.
What would be the opposite of "fork", something like "zip" that gets two lists and combines them into one? Would it have a signature also starting like this: `zip<A, B, C>(...`
I think that about right. Although I think this is slightly different than regular zip which is [a] -> [b] -> [(a,b)] and instead would be ([a],[b]) -> [c] or if I remember currying right [a] -> [b] -> [c].
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.
Well it all depends on how you're expecting your types to be extended. I'm not sure what TypeScript's support for interfaces is like but it might make sense to have interfaces for both (just in case you ever have a product that's both physical and digital).
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.
As the docs mention, the effect is only material when there's a non-trivial number of union elements. For unions containing only a handful of elements the effect is inconsequential.
I totally agree with most of this list but have some clarifying and contrary:
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.
Actually thinking more about #5, there’s a class of problems where I think `any` is better than anything else: testing any kind of untrusted input type guarding. Granted `unknown` is safer in your program but `any` is most like the real world and you’re far more likely to catch mistakes in your expectations if you throw the compiler out the window and pretend it never existed.
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".
TypeScript can't always save you against yourself. But adding in the TypeGuard makes the intent explicit. If your guard isn't actually verifying the thing has the structure you say, of course it's going to fail. But at that point your code was going to fail anyway (say if you just cast it to the type), with a less-useful error message.
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.
What's the difference between failing a type assertion vs failing a type guard? Both fail at runtime, right? So I don't understand how one would be better than another. Type assertions seem pretty explicit to me (saying you want this result to be as a certain type).
A failing type assertion that correctly fails because the input is not that type is good: it lets you divert to another path. E.g. if it's Type X do this, if it's Type Y do that. Or even just put up a vaguely-useful error to the user: "The third-party service didn't send the expected data" instead of throwing the obscure error `Cannot read property 'prop' of responseData` in the console and freezing.
Regarding the "bangbang" operator: I agree that the syntax is overly cute and frustratingly confusing to new users of the language, but I still find I often want a "not falsy" operation. I've moved to using the `Boolean(variableToCheck)` pattern. It's certainly far more characters to type (without autocomplete) than the pithy double not "bangbang", but benefits from being explicit in what it is doing and benefits from some small performance fast paths in some JS engines over !!. (Micro-optimizations like that are rarely a good primary reason to switch to a different tool, but it's a nice side benefit from all the other reasons including and especially making the code easier to read.)
I think #6 (optional properties) is a pretty contrived and unrealistic example. Most people use optional properties for extra options or enhancements, not as substitution for an object hierarchy.
I've definitely used them in the way the article describes. Think about an API that gives a list view and then a detail of an object that has more properties than are sent in the list view (maybe because they are expensive to compute or maybe just to save bandwidth). This scenario can definitely be more accurately modeled with interface inheritance than with optional properties.
Okay your answer is more illustrative to me of the problem and solution. Totally agree, if you've got a list view and detail view those should be separate classes. Though my personal take in that senario is you might be better off using TypeScripts mapped types to define a subset of your detail view as a list view.
Agreed. The author is conflating discriminated unions with optional properties. His example is better off as a discriminated union because the optionals are set based off of it's type. There are plenty of situations where optional properties do make sense. Particularly where a default value can be used.
Hi everyone! Happy to see my article shared here. If there are any questions, just let me know.
And yes, the list is opinionated ;)
Especially the question on one-letter generic types has been debated a lot.
Regarding "don't use `x as Y` but instead use type guards".
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.)
Don't we mostly see this from json responses? The example they gave adds overhead, checking each product form an array. We use some responses with 200 products, we don't want this extra latency.
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).
If you're receiving JSON data, you can not validate the data at compile 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.
Type guards do have a runtime overhead (since they're functions that get called), but so does any other method of validating untyped data at runtime. That's probably the 99% case with type guards, so I feel like the specific overhead on that isn't such a huge deal since anything else (e.g. runtypes) would probably have more.
(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.)
The case I mentioned didn't have to do with validation - in a case like that, I would have used a type guard, or more likely a runtype. This was a Javascript corner case that I hadn't previously encountered, where if you load the same module from two different paths, they aren't `instanceof`-equivalent. In this case, both the codebase of a module I was working on, and the codebase of one of its consumers, imported the same module from the same package, and I ran into the issue when I `npm link`-ed the module into its consumer's node_modules dir for testing and to ensure I hadn't inadvertently broken an implicit contract. The `instanceof` mismatch, between an object instantiated in module code and the class imported by the consumer, confused the type system and gave me spurious errors as a result.
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.
TypeScript got `!= null` wrong. In Flow (and CoffeeSript) null and undefined are treated the same, and `!= null` is preferred. You do not want to deal with code that treats null and undefined differently.
I think the suggestion is for typescript to have one none type equal to undefined | null. The type checker could not error on undefined when null is required or vice versa.
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.
Nah. This doesn’t change the semantics of JS. As numerous sibling commentors pointed out, distinguishing between null and undefined is not a good idea in normal JS either. This was simply a bad design decision in TS, especially given that CoffeeScript has done it “right” many years before.
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).
Type guard in the example should use runtime type checkers or codec like io-ts (https://github.com/gcanti/io-ts), otherwise it will just become a type-assertion with partial correctness.
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).
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.
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 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.
Static typing check is a huge boost for code refactoring. It allows you to quickly make changes and be reasonably confident that you haven’t horrendously broken something.
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.
Yeah, I mean I don't want to just say "you're wrong", but you kind of are here.
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.
There's a heavy push towards Typescript lately and I agree with your counter. I'll add to it: Type checking gets you somewhere, but are you better off there? It depends on where you want to be.
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.
I'm surprised the list doesn't mention untyped-exception and how to deal with the inability to figure out the type from the catch block.
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
About #2
Wasn't so sure about typescript so I tested it to confirm that it worked.
But in Python3, the default arguments are evaluated at module time loading if I am not mistaken. The following will display roughly the same timestamp
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);
Which also means the suggestion to use ?? has a different result then using a default param. ?? will catch both null and undefined, where as a default parameter will only trigger on undefined.
Yes, the logging event is younger, the timestamp displayed is of greater value. Considering the epoch time as the date of birth the displayed timestamp is older.
Probably a translation issue from my native language.
There seems to be a lot of people finding that they have to use the "any" escape hatch quite frequently. Our team has found we practically never use this anymore since starting to use tools such as "zod" to simply and quickly define schemas for input data, such as JSON, which we can then use to parse/validate untyped data into typed data to satisfy both the compiler, and us, that the data is the shape we expect.
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.
I think the code sample after `Use the new ?? operator, or, even better, define the fallback right at the parameter level.` is not correct, it stops after new Date without any mention of ??
The before and after code examples for #2 are not equivalent. Default parameters are only applied if no value or undefined is passed. In the case of the value being `null`, no default parameter is assigned, but using `date: date || new Date()` would assign `new Date()` as the value.
I've got a team that insists all types go into `.d.ts` files. For every type internal to the app (an API backend). The types are never exported, not even to the frontend. Apparently because the tech lead thinks importing types is noisey.
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.
Nice article, but all the negatives made it a little hard to read. For some points it was hard to figure out whether the author was in favor of doing or not doing it.
As much as I agree with most of this, I also think that some of it can lead to some weird issues for new devs or dangerously, lead to a world where people are helpless once they accidentally break out of the world of typescript. In a few cases, there is actual runtime overhead for the choices made, for example the typechecking example.
The section about preferring setting defaults in function parameters or using the `??` operator glosses over the fact that these two strategies have different behaviors for the value `null`.
I don't know who uses `!= null`, but just... Ban the != operator from your codebase altogether using a lint rule, in favour of !==. The severity of the bugs you'll avoid very much outweigh the muscle memory you'll need to develop to type a comparison to the actual value you want to match against.
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.