I recently saw a code example in the VSCode repository where Extract was used in a really awesome way. Say you have a complex class with lots of fields and methods, and you want something that's a specifier for either a constructor's parameter or a query system. Often times, that will be very similar to the underlying class, but just the fields in it - excluding every member that's not a function. Instead of rewriting every single field name for your new type, just have your new type be Exclude<MyType, Function> or Partial<Exclude<MyType, Function>> and the resulting type is perfect for your needs.
Pick is also really useful if you have an interface that passes down a subset of complex things you get from a library; no need to retype their types, just extend a Pick of the library type.
Funnily enough, I have done a session today where I live-coded using the utility types in an effort to explain most of the types listed on that page to a few of my colleagues.
Typescript is actually a great language. And with those utility types, you can do pretty fun stuff like, for example, you want to mutate a type so that some fields become mandatory:
type Ensure<T, K extends keyof T> = T & { [U in keyof Pick<T, K>]-?: T[U] };
class A {
foo?: number;
bar?: number;
baz?: number;
}
type MandatoryFields = "foo" | "baz";
type B = Ensure<A, MandatoryFields>;
const b: B = { foo: 42 };
Why write code like that, instead of extending the class with a mandatory property? The above code is going to be inscrutable to a lot of engineers, and this isn't something like an ORM where there's a good reason for that.
A better example is `Partial`, which makes all properties on an interface optional. Lots of use cases for that, like creating a `Dictionary` type that forces you to check for undefined values, or allowing you to support partial-updates to types without having to repeat your interfaces.
The other thing is that types are more often used than read. You don't need to read the `MandatoryFields` type definition often, because your IDE/typechecker will automatically enforce the contract and tell you when you're missing properties.
TIL, interfaces can extend classes in TypeScript. [0] If interfaces could not extend classes, that would be a reason to use type programming.
Another reason could be a generic interface. If you have a lifecycle where a type is mutable at one point but immutable at later points, you could use mapped types to enforce those constraints on the class methods generically.
Utility types are useful for example in the React API. You have a "state" defined as a set of properties. Then you have a setState() method where you return the set of properties you want to update, which may be a subset of the full state. So if the type of the component state is TState, then the return type of setState() can be defined as Partial<TState>.
The advantage here is that you are not repeating the type of the property twice (once as optional in the base type, once as mandatory in the extended class).
Even though the code may seem inscrutable, note that the resulting type is fairly easy to understand in your IDE. That is, if you hover over the "B" to see what the type definition is, you see:
type B = A & {
foo: number;
baz: number;
}
If you defined the type like this (which is equivalent to extending the class as you were proposing) and later on someone changes the type of one such mandatory properties in the base and/or extended class without changing the other, the error becomes much much weird, on the lines of:
> Type 'number' is not assignable to type 'never'.(2322)
Here typescript is saying that a prop cannot have a value (type never) because the base class defines it as "number?" but the extended one defines it as "string", and the intersection between them is empty. This is hard to understand when it pops out where you don't expect it. Harder than ignoring the weird "Ensure" thing, seeing what it does (the resulting type B definition) and moving on.
Defining advanced types may be cumbersome, but dealing with code that uses them is still approachable. This allows the more experienced team members to "shape the ground" and less experienced members still reap the benefits even if they don't fully understand how the thing works.
The beginner programmer copies the property definition.
The advanced programmer simply writes "type Ensure<T, K extends keyof T> = T & { [U in keyof Pick<T, K>]-?: T[U] };", thus removing the need to copy the property.
The master programmer copies the property definition.
Consider it in the context of a framework like React. The type of setState() is defined as Partial<State>. The framework cannot just copy paste the type definition with properties set to optional, since the state type is defined by the user and specific for each component. Without type helpers there would be no way for a framework to define the type for setState().
I'm not sure how often you would need type helpers in application code, for frameworks and libraries they are a godsend.
Your argument is a bit like saying we don't need parameterized types like Array<T> because you can just copy paste the code for each type.
Do you have some argument? Without arguments, your comment has as much substance as me replying:
The beginner programmer copy/pastes the code.
The advanced programmer writes a function, thus removing the need to maintain copies of the code.
The master programmer copy/pastes the code.
It is not like "Ensure" would be a single-use thing. "Ensure" here is a utility type definition, which is what a "function in the world of types" would be.
I think they're going for a "The Codeless Code"[1] type of koan. The idea is to express through something that may seem slightly illogical or counterintuitive an insight.
> Without arguments, your comment has as much substance as me replying:
Yes, what you substituted is equivalent, and they likely could have written that to the exact same effect.
> It is not like "Ensure" would be a single-use thing. "Ensure" here is a utility type definition, which is what a "function in the world of types" would be.
There's a few ways to interpret the stanza. The way I interpreted it is that the beginner uses a library to provide the definitions, the advanced programmer just writes their own definitions inline as needed, and the master programmer uses a library for the definitions they need (whether written by themself or someone else).
In that respect, I think you're both in agreement.
For what it's worth, I think comments like these are generally beneficial, if maybe I prefer at least a line of context. That they can be interpreted differently and may require a bit of thought to map onto the current context can sometimes allow people to view their beliefs from a slight remove where more introspection is possible, or spur interesting tangents to explore. Both of those are generally beneficial in a forum like this, IMO.
I'm building a strongly typed form abstraction layer for work. I use code like this to express "if this generic can be undefined, this field is required. Otherwise it cannot be used".
So: FormElement<string|undefined> needs to have a "disabled" function, indicating conditions under which it becomes disabled (and absent from the model), while FormElement<string> must not have a disabled function, as it will always be present in the model.
One pitfall of this approach is it requires a lot of trial and error to find the incantation that both works and doesn't swallow error messages.
This type takes two type parameters, one called T and the other K which will consist of Keys belonging to the type T (in our case, "foo", "bar" or "baz").
= T &
This new type (called Ensure) will be equal to the union of two types: One will be T and the other will be:
{ [U in keyof Pick<T, K>]
A new type which keys will be picked among the key listed in K
-?
To which we will remove the potential optional qualifier
Most of the implementation details here don't really matter until you need to modify these advanced types directly. That Ensure type definition line in that example is a low level detail that you put in a library somewhere, import throughout your codebase, and then mostly forget about.
In practice you'd have someone that understands this set it up once, and then document its usage for others, maybe document the implementation to make it easier to modify later.
The TS compiler is surprisingly good at giving you good readable error messages as well when your code violates these advanced types; the errors tell you what you specified and what is supported, it doesn't display the low level type logic as part of the error users see. This means that there's very little need for anyone to really how these type definitions work.
Until it's the root cause of code not working as expected, by another developer far removed from initial implementation. Non-obvious code is harder to maintain. Code is written for people, not machines; that means the harder it is for people to maintain, the less useful it actually is.
Valid point. On the other hand, these kinds of advanced types can prevent lots of bugs and maintenance work, and may therefore be worth the day of debugging when it breaks after two or three years of usage.
I've used types like this in a pretty advanced TypeScript UI project consuming lots of services to enforce compile time errors. We were using generated TS clients for all of the APIs we consumed, and the compiler would automatically throw readable errors wherever we were missing form fields or types became incompatible. I committed the advanced type once, documented its usage, and I don't think anyone has had to deal with it since, whilst the types have steadily prevented errors.
And even then: it's just type definitions. If it really becomes a maintenance burden or someone has no clue what it does, you can simply replace the type with "any" or something similar and all of your problems are gone and typescript won't complain anymore (at the expense of less type error checking).
Complicated types (like any complicated code) need their own tests demonstrating that they do what the author thinks (and fails to think about, as it's changed).
> In practice you'd have someone that understands this set it up once, and then document its usage for others, maybe document the implementation to make it easier to modify later.
In practice, that someone then leaves the company, leaving this nightmare underfoot.
> The TS compiler is surprisingly good at giving you good readable error messages as well when your code violates these advanced types
Only if you're that original person who understands it! I would still have no idea what is happening, no matter how clear.
"Type '{ foo: number; }' is not assignable to type 'B'.
Property 'baz' is missing in type '{ foo: number; }' but required in type '{ foo: number; baz: number; }'."
I've had the compiler emit errors much like the following for way more complicated types that combined several of these kinds of structures together to form much more bespoke type checks (reproduced from memory, so I'm not 100% certain on the error or use case):
const e = form.email
^
ERROR: "email" is not in '"name" | "firstname" | "lastname" | "e-mail" | "birthdate" | "password"'
The thing to note here is that it often doesn't expose the details of the implementing type and underlying (admittedly complicated) type system primitives at all to users. That said, I'll have to be honest and say that I have seen it throw much more difficult to understand nested errors referring to the underlying type implementation when I was working on the type system itself to create stricter type checks for functionality that was previously unchecked (i.e. treated as "any" by the compiler).
The other thing to note is that these things are really only doing type checking. If it becomes troublesome and it does start to spit out type errors incorrectly, throw unreadable errors, or otherwise become a maintenance burden, these types are not particularly difficult to remove, and by removing them you won't break your code. Consider that to be the equivalent of removing a linting rule or no longer requesting a review from a colleague. Though it's probably a good idea to document how to remove these advanced checks for when people find them annoying when someone leaves ;)
Incorrect type checking implementation is probably the biggest problem with these things getting complex, though. If your type check is incorrectly throwing errors for implementations that don't contain any errors at all, that's going to set you back a lot!
Sure. But this is easier to understand, albeit means repeating code:
class B {
foo: number;
bar?: number;
baz: number;
}
I know these are toy examples, and I'm sure there are reasons why in the real world you'll be modeling things where this is not a good option. But I'd really need to be convinced that the CRUD webapps a lot of us write actually benefit from having this in our source code.
Probably depends upon your job. The main challenge of being a programmer, in the sorts of projects I work on, is communication - with customers, management, and other developers.
I guess it'd be OK as long as everyone always accompanied such lines with a comment, with at least one line per symbol or character it's identifying & explaining. As all but the most trivial regexes warrant.
I don't have it handy but with template literal types I was able to have a type of "stripped strings" (that is, strings without leading or trailing whitespace) that seemed surprisingly usable - string literals would match (or not, as appropriate) with no boilerplate, while dynamic strings would need to be fed to a cleaning function.
I never put it in production, partially because of concerns over maintainability but far more because I had no need for it.
Depends on how much you can learn on one day I guess, but TypeScript have lots of features which should be learned before Utility types. For example type parameters, type unions, the role of null and undefined, type assertions etc.
TS has been around long enough that it suffers from the 'obsolete tutorial' problem (one I first observed learning C++): many of the utility types didn't exist when many of the popular tutorials were first written.
I really don't get the point of operations on types such as 'type TodoInfo = Omit<Todo, "completed" | "createdAt">'. Is this a real use case? Why not just literally write down the properties? Surely easier to read and in the end maybe even easier to maintain.
It depends on what your anticipation of the future evolution of TodoInfo type is.
If you think it will have more special fields like completed and createdAt that you don't want to pass on in most contexts, then it's better to list the fields that you want to pass on.
But if you expect it to not have more of those and rather get (or loose) more fields that you want to pass on, then it's better to list the ones that you want to omit.
Another trade-off might be that when the new field is added and you'll forget to update the other pieces of code, will it do less harm to pass the field you intended to omit, or to omit the field you intended to pass. Or which error will be easier for you to discover in your use case.
the only complain have with these is that there is no namespace or anything that allows me to discover them. You have to follow all the release notes or check lib.d.ts. In comparison, flowtype prefixes them with $, so when I type "$", I get the suggestion.
I'm so happy powerful type systems are more popular now. TypeScript bringing a great type system to a language as popular as JS is fantastic. Rust is also a great way to get a great type system while staying in a systems programming and procedural environment. Hell, even python type annotations support union types.
I never knew the depth of type systems until the last year when I took a type theory course and a compiler course taught by a man who loved his types. So much power - I can't wait to see the future of type systems.
The grass does seem tend to appear greener in the other paradigm.
Barely typed languages like C made rigorously typed languages like C++ and Java seem appealing. The boilerplatiness of those languages made duck typing seem appealing. Writing anything nontrivial with duck typing made more elaborate type systems seem appealing.
Needing a PhD in category theory to produce a side effect will no doubt make some other paradigm seem appealing in the future.
> Barely typed languages like C made rigorously typed languages like C++ and Java seem appealing. The boilerplatiness of those languages made duck typing seem appealing.
Eh, I consider Java to be barely typed too. If you have a variable of type Foo, the type system doesn't even guarantee that you have a Foo in there (it might be null). The whole point of a type system, in my mind, is to guarantee that I have that Foo!
> Writing anything nontrivial with duck typing made more elaborate type systems seem appealing.
In my mind, this makes type inference seem appealing, not duck typing (which is not well-defined, but most people associate it with dynamic typing).
> Needing a PhD in category theory to produce a side effect will no doubt make some other paradigm seem appealing in the future.
This oft-repeated exaggeration needs to stop. Using monads does not require a PhD in category theory. If you can understand Promises in JavaScript, then you can grasp how IO works in Haskell.
I think they work well as long as they're reasonably specific, reposting the famous HN Dropbox comment also works pretty well on HN. Other redditisms don't fare so well, and for that I'm thankful.
Not arguing for or againts something, this is just something I wanted to say.
Framing matters here, the API of Promises in js maps almost nicely to the common monad API of bind (then), join (implicitely done by the runtime whenever possible), and return (Promise.resolve). Learning any monads is unlikely to be harder than this as these operations are indeed quite intuitive.
What is often referenced with learning monads is instead Learning All the Monads, sometimes adding a touch of Learning All the Monad Transformers and interactions of different monads. This intrinsecally needs to be done in a generic/parametric way and is way harder.
To my knowledge Haskell is the most mainstream[1] language only typed language that allows expressing the categorical definition, most other type systems are significantly more limited in how much maths/category theory they can express and often focus on specialized functionality with practical implication like Rust's ownership system or Typescript's Capitalize<StringType> that only exists to allow nicer typing of some common API desings[2]
[1] most mainstream typed language at least, you can implement Monad is JS/TS but the language cannot express it the same way C cannor express generics even if you can manually implement them in it.
Monads and Promises are not comparable.
The IO Monad fulfills a similar role to Promises and people learning Haskell grasp them immediately.
Haskell also has an equivalent of the async/await syntax called the do notation which makes things nice and easy to read.
You don't really need to understand monads to understand the IO monad or Promises.
If you want to understand monads, look at their signature, mainly the bind operator and try to implement something with it. A logger, a list, the IO monad itself.
It's not that hard, it's just that nobody bothers playing with it and just try to learn the theory without experimenting with monads in code. After a few tests you'll build an intuition for it and you'll get why they call them programmable semicolons.
Monads is just one of the abstractions that can be used to implement IO in Haskell btw, it was just the authors flexing their category theory that got us in this situation.
At the same time, one of the reasons I learned Haskell is that it had a reputation for being hard and, boy, am I grateful for it.
Basically, think of a monad as a Promise. Rather, a Promise is more-or-less an example of a monad. (Yes, I know that due to some technicalities it isn't, but it behaves like one for the purposes of this comment)
Mapping a monad is equivalent to Promise#then - if there's a value in the monad, then it calls the function you passed to map and returns the result wrapped in a monad. If there isn't a value in the monad, then it returns itself.
For example, with the Maybe monad, if you have x = Maybe.Some(y), then x.map(f) = Maybe.some(f(y)). If you have x = Maybe.None, then x.map(f) = Maybe.None.
With Promises, if you have x = Promise.resolve(1234), then x.then(x => x * 2) = Promise.resolve(1234 * 2). If you have x = Promise.reject(new Error('abcd')), then x.then(x => x * 2) = Promise.reject(new Error('abcd')).
The IO monad is extremely similar to Promises - you call an IO function and get an IO monad as a result, then map that monad in much the same way you'd then a promise.
The problem with monads is they are only really practical in a language with built-in syntactic sugar to cover the boilerplate. In any other language you will surely go "whats the point?" because using monads will invariably turn simple code into a convoluted mess for no benefit. Promises on the other hand will seem immediately useful if you have tried writing async code in an ad-hoc manner. Promises solve a problem.
Mathematicians and Haskelites tend to explain things by giving their definition. (Imaging a Haskelite explaining how to write "hello world" in C: First you need a "main" function. A function is a process or a relation that associates each element x of a set X, the domain of the function, to a single element y of another set Y (possibly the same set), the codomain of the function. etc etc )
But most other programmers prefer to understand things by understanding the problem they solve. It is quite obvious what problems Promises solve, but in the context of JavaScript, monads does not solve any real world problem. That makes them hard to grasp for a programmer, even though the concept is simple.
Monads are a particular pattern for method chaining or function composition.
Here is an example of some JavaScript code which use regular method chaining:
[1,2,3].map(a => a + 1).filter(b => b != 3)
This code results in the array [2,4].
Similar code following the monad pattern would look like this:
And the result is the same. But obviously the monadic version is more convoluted and harder to read. But if there was some syntactic sugar which covered the boilerplate, then the monadic version might be bearable!
The "power" of the monadic pattern is that the operations can be chained or nested in a more flexible way. For example here the operations are nested, but the result is the same:
This does have some nice properties, since operations can be chained or nested together to composites which have the same type as a single operation. The question is if the benefit outweighs the cost in code complexity.
The monad pattern is purely concerned about how operations are chained together structurally, it is not about what they does or what types are involved. In this example the type is Array<T>, but it could be any parameterized type.
Monads can be used anywhere a sequence of operations is stringed together. (But that doesn't mean you would want to.)
Monad imho is deliberately badly explained to preserve the mystique and the smugness of the cognoscenti. I found this book invaluable for translating the field into something that makes sense: https://alvinalexander.com/scala/functional-programming-simp...
I would go further. Using monads, or any functional programming concept that you'll encounter in the wild, requires zero category theory. I would actively warn anyone against learning category theory for day-to-day use of FP. Learn it if you're interested in it for its own sake sure, it's a great maths subject, but it's basically irrelevant for most programmers imho and a distraction if you're trying to get a simple practical understanding.
I'd love to see an explanation of monads that accurately captures their capabilities in terms no more complex than those required to do the same for promises.
If you understand promises you pretty much understand monads already since promises are more or less a type of monad.
Monads represent computation contexts. In the case of promises, the computation is preformed in the context of a value that will be available some time in the future (or not at all in some cases).
my_promise.then(compute_result)
Another context could be a list, where the computation is performed on each value in the list
my_list.then(increment)
Or the context could be that the value is maybe null
maybe_string.then(uppercase)
How the computation is actually performed depends entirely on the monad. Usually .then is called .bind or .flat_map because it will automatically unwrap nested monads.
Monads are chainable containers for a computation. The container represents some sort of "effect" implicit to the computation ie asyncrony, optionality etc. This is not technically 'correct', monad explanations are always a bit cursed, but I find this a pretty useful intuition to work with day to day.
To understand Monads, we have to first understand Functors, Monoids and the Applicative type classes (type classes are more or less the same as interfaces in Java, C++ and the like).
A Functor is something that implements a `map` function. You can think of a Functor as anything that can encapsulate/surround something else. The map function applies a function to the encapsulated element without modifying the outer structure.
For example, a List is a Functor, that has this map function implemented in most languages. It indeed surrounds a given type (zero or more item of that type to be more correct), and it indeed applies the same function over each element of the map.
Several languages have a “Result/Maybe/Optional” type, that can either contain one instance of a type, or Nothing.
Some languages allow you to modify the inner element, when it exists, that is, it is also a Functor that encapsulates an element and has a map function to change that.
Let’s dissect this Monoid word next (which is not a Monad!). Before the definition, let’s look at an example: summing numbers. Let’s say we have a list of numbers, and we want to calculate the sum of it. We can start from the beginning and go through the list one by one, or sum the first half and the second half first, and then add them together. These are possible because the add operation is a Monoid over the numbers. What that means as an interface (type class) is, that it implements an `empty` function, a so called neutral element (0 for addition), and a `concat` one (which is + itself).
These names make us think of Lists, and indeed, Lists are Monoids as well, not only Functors. They have an empty method returning [], and they have a concat function that concats two lists together. Do note that concating an empty list to a list doesn’t change it.
Is a Return type a Monoid? We could make the case for empty being Nothing, where concating Nothing changes nothing, but what about concating two Results both containing an integer? Should we sum them or give the product, or write them next to each other?
It turns out that (in Haskell at least) you can do specify something like, this is only a Monoid if the embedded type it has is a Monoid. So for example that way a concat(Just [1,2], Just [3,4]) will return the concatenated list inside a result type.
We are almost there! Let’s tackle Applicative now. What can we do if not only are data is encapsulated in some structure, but even our function which we want to apply to?
Can we apply a list of functions over a list of values? Or an Optional/Result function over an Optional/Result value? Does it even make sense?
Let’s define Applicative as being a Functor that has a `pure` function similar to our Monoid, as well as a `sequentialApplication` one that we will shorten as the cryptix <>. Since it is a Functor, remember that it also has a map function available.
The pure function simply encapsulates a type inside itself. For example, a list constructor is precisely that, like python’s list(2, 3) creating a new list. The <> is a bit more complex, it takes as parameter a function that is encapsulated in this very type and operates on type A. Its second parameter is an encapsulated object containing type A. And the important thing comes here: it will apply the encapsulated function to an encapsulated data, without unwrapping first.
Let’s say, I have a text field where the user Maybe entered his/her name (sorry) and a field where we await an age. We’ll store these inside a Result<String> and a Result<Age> type.
Let’s say we have a User constructor that awaits a name and an age. We could handle each parameter separately here, but what if we have 20? So we instead do something like pure(createUser) <> nameResult <> ageResult. This magic line will apply the Just createUser function (that is, wrapped into a Result with an existing value due to pure) to a possibly missing name. Let’s stop for a moment here and talk about currying. In many FP languages, calling a function with less parameters is not a compile time error. It gives back a new function that awaits one less parameter. Eg `+ 3` is a lambda that got its first parameter as 3, and will “execute” when we give it the second parameter.
Knowing this, our evaluation so far will be something like Just (createUser(nameResult if it has a value)) or Nothing. Now we apply this, yet again enwrapped function to the last parameter, so we will get as a resulting type a Result<User>, which will contain a user when both values were sent, and will be Nothing if any of them were absent - cool isn’t it?
But I know, we are here for Monads!
Well, Monads are just monoids in the category of endofunctors. Just kidding. They are Applicatices, that also have a `bind` method.
So you’ve seen how we could “sequentially apply” functions. But the “problem” with Applicatives, is that we can’t depend on the output of a previous function — in the previous example we could not have ageResult’s evaluation change depending on what was returned previously. Let’s see another Applicative, the often misunderstood IO.
putStrLn has the following type: String -> IO ().
That is, it waits a string and gives back an IO structure that returns void (actually it is called Unit). If we were to somehow execute it, it would print that string ending with a newline. If we do
(x => putStrLn(“world”)) <*> putStrLn(“hello”), it will output (if we know how to execute it) hello world in two lines. The reason for this strange ordering is, that we apply a function that drops its first parameter to the second one. (Haskell does have a shorthand for this!)
But how can I act upon the result of a previous computation in a “sequential application”, eg. read in a line and print hello $name? By `bind`! It does the following: it needs a monad at hand, with encapsulated type A (eg. IO String for the readline we will use), a function that takes that type A (String) and returns this monad with any type (we will print out the string so we will use putStrLn)
So, bind(getline, name => putStrLn(“hello $name”)) will do what we want and you have just used your first proper Monad (Haskell of course provides a nice syntactic sugar over this called do notations)
With these abstract structures, IO can be dynamically constructed and side effects will only happen where we expect them.
Not an explanation, but they key thing to know is that monad is not a thing, but a pattern. It's a pattern for composing things. Like the FP equivalent of the Unix shell pipe character. Like the Unix shell provides composability for processes that happen to read from stdin and write to stdout, most FP languages provide a framework for composing monads (of whatever kind) neatly e.g. Scala and Haskell "for comprehensions".
Monad:
In functional programming, a monad is an abstraction that allows structuring programs generically. Supporting languages may use monads to abstract away boilerplate code needed by the program logic.
Promise:
A Promise is an object representing the eventual completion or failure of an asynchronous operation. Essentially, a promise is a returned object to which you attach callbacks, instead of passing callbacks into a function.
> Eh, I consider Java to be barely typed too. If you have a variable of type Foo, the type system doesn't even guarantee that you have a Foo in there (it might be null). The whole point of a type system, in my mind, is to guarantee that I have that Foo!
That seems a rather arbitrary limitation. When Java says Foo, it would translate to what you would consider Maybe<Foo>. How you represent types, and what that representation implies is a matter of semantics.
That said, Java's type system is pretty dang weird, especially generics.
> This oft-repeated exaggeration needs to stop. Using monads does not require a PhD in category theory. If you can understand Promises in JavaScript, then you can grasp how IO works in Haskell.
The concepts themselves aren't particularly hard to grok, but the more academic side of functional programming (i.e. Haskell) is comically inaccessible, mostly due to jargon (monads aren't even that bad compared to something like kleisli arrows).
"Maybe<Foo>" doesn't have the same problem as Java nulls though, because with an optional type the type system would force you to always handle the possibility of the value not being present. With Java nulls you just get a runtime error if you try to do certain things with something that turns out to be null.
I think the major shift was that originally types were used mostly to talk about representation. Then we wound up in a situation where caring that much about representation didn't make sense as often (at this point it's at system boundaries and when we care an unusual amount about performance) and so it made sense for some languages (covering a growing portion of programming) to stop talking about representation. But it turns out there are other useful things that we can use similar technology to "talk about" as we learn to build better tools and to better apply them.
I mean, when you look at a problem that monads solve with types is that every function has an “annotation “ of what it uses (IO or mutable state). Similar how async in JS allows await, a state annotation would allow put get or IO annotation any of the IO capabilities.
Of course monads are much more but Mut does not look like it pollutes everything with Mut, including function definitions and results
TS has good option of being optional. I programmed C++ (rigid niminal), Ruby (duck), Haskell and TS is best compromise so far as it works like tested documentation which is most practical for many purposes (where program must be iterated). If you know what you want to build beforehand then sound typing might be better.
C is not "barely typed". It has quite a lot of type checking.
An expression like "obj.memb" in C requires obj to be declared to have a type which has a member "memb".
C catches it if you call a function with the wrong number of parameters, or wrongly typed parameters, such as passing a "struct foo *" pointer where a "struct bar *" argument is required.
C has "holes" in the static safety net in areas like memory safety: object boundaries and lifetimes. It allows some unsafe conversions, like any object pointer to a void * and back. But not only those: C has unsafe numeric conversion and operations.
Still, there is a type system there, and C programs greatly profit from it; it's the big reason why we have so many lines of C code in our computing infrastructure, yet the proverbial sky isn't falling. (Just the odd lightning or hail here and there.)
C compilers also help; modern compilers have a lot more diagnostic power than compilers thirty years ago. In C, it is critically important to diagnose more than the bare minimum that ISO C requires. For instance, whereas a function that has not been declared can be called in any manner whatsoever (any number of arguments), it's a bad idea to do that without issuing a diagnostic about an undeclared function being used. If such a diagnostic isn't enabled by default it's a bad idea not to add that. C programmers have to understand the diagnostic power of their toolchain.
Recently, GCC 11 found a problem in some code of mine. I had converted malloc/free code for a trivial amount of memory to use alloca. But somehow I left in a free call. That was not diagnosed before, but now it was diagnosed.
Another obscure bug that a newer compiler with newer diagnsotics caught for me in the last few years was a piece of code where a comparison like this was being made:
d <= UINT_PTR_MAX
where d is a double. The idea was to try to check whether d is in the range of a certain integer type before converting it. Trouble is that the above expression moves the goalpost because when UINT_PTR_MAX is 64 bit, then its value is not necessarily representable in the double type. What happens is UINT_PTR_MAX is converted to double, and in that process it goes to a nearby double value which happens to greater than UINT_PTR_MAX! And so then the range check becomes wrong: it includes d values in that extended range, which are beyond the range of that integer type, causing undefined behavior in the conversion.
In the field of formal type systems two common approaches to defining types, in practice they are quite similar but in my opinion they differ a lot in framing.
One side can be represented by Haskell, Hindley–Milner type systems, or even Coq; here every value has its own "best" type that is intrinsecally associated with it, that is values and types are defined and constructed together.
On the other side you have sort of a formal definition of duck-typing; you have values and properties that are satified by some set of values, here you have your values (all numbers, all strings, all memory addresses) and expres in usual logic terms any property you want (e.g. this memory address must be either Null or point to a string of even length).
All this to say that C has a nice type system from the first point of view (function pointer allow you to have higher order functions!) but a very weak one from the second point of view in that it is very hard to decide if an operation will have a valid result just by the types of the values you feed into it (let's not talk about UB for now).
In my opinion in later decades there is a movement to care more about type systems that follow the second approach. In my opinion it is one of the reason for the success of Typescript; its objective wasn't to have a nice type system full of good properites, but to model how javascript was being written.
Here's another. Instead of returning Sometype|undefined from a function which may or may not have a value to return (such as searchCustomer), return Sometype|null.
That forces the function to return a value that's explicitly intended rather than defaulting from a missed out if-else codepath. This is useful since JS is often imperative style code.
The difference between null and undefined in JavaScript is something I wished had never been implemented. Other languages refer to null as their billion dollar mistake, but somehow JavaScript got 2 of them with slightly different but sometime identical behaviour. I would defer to eslint to prevent this particular issue if you care about it, this allows you to set rules in your own code without any impact to the outside world.
I have only seen null vs undefined lead to 2 things in my experience: mistakes and bikeshedding.
> Other languages refer to null as their billion dollar mistake
The "billon dollar mistake" as described by Tony Hoare was not nulls per se.
The billion dollar mistake was having a type system where null was a member of every reference type. This does not apply to language like JavaScript without static type checking, and it doesn't apply to type systems like TypeScript where null or undefined have to be explicitly specified as members of a type.
The undefined/null distinction solves an additional problem: In Java you don't know if a value is null because a field wasn't initialized correctly or because it was deliberately set to null. JavaScript allows you to distinguish between these two scenarios.
It it just a mistake or oversight in the definition of the language. It doesn't really make logical sense that a null is an object but a string is not an object.
It would kind of make sense in Java, where only object types can be null. But that distinction does not exist in JavaScript.
typeof null should be "null", just how typeof undefined is "undefined"
it is an implementation bug of netscape, null was represented by the zero pointer and objects where tagged pointer with a tag of zero, so when reading its tag null looked like an object.
there are solutions as x == null, x === null, and Object(x) == x allow you to check for null|undefined, null, and object values, but typeof null being "object" is purely a specification bug that it is too late to change.
I've always liked the two-nulls solution in JS. `undefined` is a runtime-generated missing value, whereas `null` is a compile-time author-supplied missing value. In other words `undefined` is a "pulled" missing value, `null` a "pushed" missing value. Any feature can be misused, but having the distinction is certainly helpful.
I'm fond of this distinction as well. One could also parse it semantically as `undefined` meaning "unknown unknown" vs `null` being a "known unknown" (or "this value left intentionally blank").
Where I think it falls down in practice, is that JS still treats undefined as a legitimate pseudo-value, as opposed to a read-only return result for a missing key. So for instance, `x=[0,1]` and `x=[0,1,undefined]` will both return undefined for `x[2]`, and it takes jumping through some hoops to know if that value was undefined on purpose, or if the key is simply not found.
If I had my druthers, attempting to set a value as undefined would either throw a fatal error, or be an alternate syntax to unset a value (such that `x.length` would equal 2 in both examples above).
When people refer to the "billion dollar mistake", they mean the language feature that every value could potentially be `null`, i.e. even if a function returns MyType, it could also return `null`.
TypeScript in strict mode (the default) still has `null` and `undefined`, but not the billion dollar mistake: if you want to be able to pass `null` to a function, you have to mark that parameter as being potentially `null`.
> I have only seen null vs undefined lead to 2 things in my experience: mistakes and bikeshedding.
I disagree, though I think the implementation leaves something to be desired. Primarily, I think there is fundamentally a difference between the value of obj.bar in the following examples that is useful to differentiate between:
{ foo: 'hello' }
{ foo: 'hello', bar: null }
For example, GraphQL makes specific use of this when dealing with input types for mutations: null essentially means "delete this field" while unset means "don't change it".
There is a very good discussion on this topic here, https://github.com/graphql/graphql-js/issues/133 , which goes into the rationale behind it, how it's supported in languages that do NOT differentiate between null and undefined, and how some folks changed their minds on the issue.
Both Flow and CoffeeScript got this right, while TS is slowly dragging its feet towards the right solution: Pretend there’s no difference between them. Your code will be easier to reason about. If you need two different “other values”, use a proper enum / type union / restructure your API.
I disagree. `null` in TypeScript is equivalent to `None` in many other typed languages. `undefined` in Typescript is like null in other languages, with the caveat that if you’re working to transition an untyped codebase and trying to bring types, there may be a useful place for `undefined` in order to express that there is a lack of safety / strict-handling in that area.
I’m still not sure about Error handling, though. Seems feasible that in a fully typed project, any possible unhandled error type could raise a compile error. AFAIK there’s nothing (beyond catch + exhaustive switch) to handle exhaustive error checking in TypeScript, nor is there lib support for handling it either.
Scala, as I recall. It encourages using Options (Some/None), but since it runs on the JVM and will often interop with Java libraries, you can also have nulls.
Not exactly a language design, but an unfortunate reality.
javascript, at least, has "undefined" and "null", an infuriating duality of falsiness. PHP also has a notion of not being set as well as being set but null.
I prefer to use undefined over null since it just fits more naturally with other TS features like optional fields. But I agree with using tsc’s no implicit returns check.
You can declare Sometype|void return type. So the compiler will check that you either don't use the return type or treat it as Sometype. Of course this depends on the logic and for failed routes you should return appropriate results.
void implies that the return type should is undefined behavior and should not be relied upon, so that something like this
const x = foo();
is incorrect when foo() returns void. 99% of the time x will be undefined (the value) but there are cases where it would not be. For example
arr.forEach(x => x.sort())
sort returns a value as well as having a side effect. But forEach expects a void callback. This code is perfectly fine in JavaScript because forEach does not read the return value of the callback.
I agree. You should type something as null if you need to force callers to deal with the null value and can't do that with an exception.
Type it as void if the value isn't really important to the caller, or you'll throw exceptions in an exceptional case.
Common wisdom is to always have user-defined functions return void, but sometimes I think it's okay to use void if you're replacing a built in JavaScript functionality so the outer code was relying on that semantic. For example, replacing a simple usage of findIndex (that returned undefined) with something more complex that does API calls.
The third example describes something useful in record types, but goes about it in what seems an odd way, and ends up suboptimal as a result. I'd instead use an object type like this:
type Human = {
name: string;
age: number;
}
which also enforces value types in the compiler, rather than requiring runtime guards.
The type itself is fine but how it is checked and used is not. The following would be better, and in this case you will also have a Human type inside the forEach callback:
// ... other code
type Human = { name: string; age: number }
const isHuman = (obj: unknown): obj is Human => obj && typeof obj === 'object' && 'name' in obj && 'age' in obj; // you can complete the gaps here and also check the property types
someArray.filter(isHuman).forEach((h) => {
// h has type Human now
console.log(h.age);
})
This is reasonable for validating an untrusted object, sure, but I don't recall that constraint being expressed as part of what the original post was addressing.
The type guard may be useful when transferring that array server->client or vice versa, or for an array from any other untrusted source, but I don’t think that was the purpose of the demo in the article. When the array in question is coming from the same code base that kind of runtime type check is redundant.
I think the example is a bit contrived with a preset union. Where it’s really valuable is when you’re extracting a union from another source (like via keyof) and want to keep the two objects in sync without having to modify the keys in two places.
Yeah, typically you want a record type for something like a mapping over potentially arbitrary keys (via an index type) to values of known type, and an object type (which can have optional keys) when you do know exactly what shape you expect and want to enforce it.
I found that pattern useful when you don't know what the key will be. I have an iOS app that tracks tips, when you add a tip, it's stored in an object like this:
type Tips = {
[tipGuid: string]: TipObject
}
which can be rewritten using Record as
type Tips = Record<string, TipObject>
That pattern ins't very useful when creating object with known keys but for data structures where the key is either not known or generated it a godsend.
It’s not true that Record will result in a type where any key is valid. If you pass in a primitive like string, then of course any string will be valid. That’s not Record’s fault; what you’re doing is essentially creating an index signature [1]. If you pass a more restrictive type in as the key, it works as expected:
type Tips = Record<“foo”, TipObject>;
const tips: Tips = {}; // error, needs key “foo”
tips["foo"]; // fine
tips["bar"]; // error, no key “bar” in tips
It’s worth mentioning that this isn’t just an issue with objects. For example, by default, the index type on arrays is unsafe:
const arr: number[] = [];
const first: number = arr[0]; // actually undefined, but typescript allows it
If you do need an index type and want to account for undefined keys, the idiomatic way is the noUncheckedIndexAccess compiler flag [2], which will automatically make any index property access a union with undefined.
IMO non constant (as defined by TypeScript) arrays should’ve been automatically assigned a union type with `undefined`, which can also be a fix for Records too:
While this is probably alright for some data, I'd definitely recommend using something like a Map instead (especially if the object mutates) for things you have control over (ie it's not describing an endpoint or something similar).
Eh. A good ORM provides type definitions from model definitions, which is one way I've found ORMs more useful in TS than JS, and I'd more likely use a runtype or a decoder to both validate and type inbound data than roll my own interface for it.
On review of documentation, I was actually pretty off base in grandparent comment. The real use case for Record appears to be when you need a map type whose keys are both explicitly enumerated and defined elsewhere, ie in a union, enum, or otherwise unrelated object type. Rather than duplicating the keys, you can use Record<someUnion, V> or Record<keyof typeof someEnum, V> and only have to make one change to update both.
For the "arbitrary keys, known value types" case I mentioned earlier, an object type with an index signature works fine and may be more legible.
Note that `Readonly<T>` does not prevent a call to side-effect methods when `T` is not among a predefined set of built-in types. Indeed, it prevents such calls only on predefined types such as arrays, maps, and sets. It could be more "accurate" to use `readonly number[]` instead of `Readonly<Array<number>>` for highlighting the difference.
It was an easy catch because I was told there's an issue, but I'm surprised const arrays don't at least have a warning there. Or even default to having readonly-like behavior
Which "const" do you mean? The one in front of a variable declaration cannot make the array itself constant. That's because that "const" only refers to that variable itself, which is just a pointer (except for the primitive types).
The variable declaration "const" means this variable cannot be changed to point to a different object. It says nothing about the thing it points to and that is how that keyword was designed in this language. It's Javascript (ECMAscript), not Typescript.
On the other hand, using Typescript (which only adds type annotations but the actual code is ECMAscript apart from very few small things such as "enums"), you can append "as const" after an array though as type annotation, as in
const arr = [1,2,3] as const;
// Type error: "Property 'push' does not exist on type 'readonly [1, 2, 3]'"
arr.push(5);
Which is the same as Readonly<type>.
This "as const" annotation can be used for any object, not just for arrays. Of course, it can only guard against known methods of mutating an object, such as direct write access to properties and known mutating function calls for known object types such as the built-in ones (Array, Set, Map, etc., each one needs the definitions for the readonly-version of its type in the Typescript-bundled type library).
> if you assumed I meant Typescript in response to a Typescript article...
You misunderstand TypeScript.
They cannot 8and will not) change the "Javascript" in Typescript. "const" is a keyword with a meaning defined by the ECMAscript standard.
Typescript IS Javascript. All they do is add type annotations. Only some old non-essential features like namespaces and enums need to be transpiled, and "enums" really is not much and should actually be handled by whatever minifier and bundler/packager you use. Other than that, if you removed the type annotations you are left with 100% ECMAscript.
Typescript was meant to be just a type-annotation extension and explicitly made the decision that the code itself would always be stock-standard Javascript.
Arguably, it was a bad design decision that now confuses lots of people about the nature of Typescript by bundling type-checking and transpiling to some target (originally for older runtimes that did not understand es2015 or were lacking some feature available in the latest JS runtimes and ECMAscript standard).
It is therefore not a surprise at all that Typescript did not make "const" into something else. The basis always is the ECMAscript standard.
This is one of annoying aspects of TypeScript. The following should work (in fact, this is how `Record<K, V>` is defined in lib.es5.d.ts after all):
type Person = {
[key in AllowedKeys]: unknown
};
Note the switch from interface to type and `:` replaced with `in`. The point is that the `in` syntax (conceptually expanded into multiple fields) subsumes the `:` syntax (a generic type ascription) and is only available as a mapped type, which is distinct with an interface type. The error message does mention this, but if you don't know what is the mapped type you are left with no clues.
I actually don't really like Record types in the way people/library maintainers often use them - the type-checker asserts that values are actually present for all the specified keys, which is fine if the objects with the Record type really were exhaustive; but instead I often see them used where the reality of the data is a Partial<Record> - some keys are missing. Something about the abstraction causes people to misuse it frequently.
You pretty much always have to define your record type with `| undefined` tacked on to the value type parameter. With that, the problem mostly goes away.
Well, it was a kind of a joke intended for the folks whose brain saw the comment the same way I did. I got the result by applying the footnote to the object, as half-suggested by syntax of the parent:
The core problem here is that you don't actually want to check if something is an object, but whether it matches the Human type. The correct way to do that is to define a type guard [0], for example:
function isHuman(input: any): input is Human {
return (
Boolean(input) &&
Object.prototype.hasOwnProperty.call(input, "name") &&
Object.prototype.hasOwnProperty.call(input, "age")
);
}
There are libraries which can automate this for you which is the route I would recommend if you need to do this often. As you can see, the code to cover all edge cases such as `Object.create(null)` etc is not trivial.
That's fairly subjective and I can see arguments for both sides, in the above example I've gone with the approach the Typescript documentation itself gives which uses any. Given the parameter is only used as an input, using any and unknown are interchangeable and there is no difference in this specific case.
While I see where you're coming from, semantic formulation like this is highly subjective. All the same the problem statement could be "Given any kind of input, tell me whether it's a Human or not".
This approach is "correct" only when you're interacting with an API you have no control over whatsoever because specifying all this and maintaining it is really error prone.
All this boilerplate is not a proof. It remains an assertion. And so in most cases you might as well just add a "type"/"kind" property to your object (or create a class).
Personally I really enjoy Typanion [0] since it's very similar to Yup [1] which I previously had extensive experience with. You can find more alternatives and a lengthy discussion about the whole problem space and its history in [2].
I very, very much disagree with the type of suggestions here. Probably because I don't know TS but here it comes anyways...
You don't need a library or check that it isn't an array or anything like that. What you actually want to check is what it _is_.
You absolutely don't care whether that object is an array or a function or what have you. What you care about in that piece of code is that it satisfies what you want to do with it.
In the context of the article you can do an ad-hoc check on the fields that you require in that context.
Aside:
There are places where you want to have a kind of rigidity around the shape of your objects, including homogeneous collections. The JIT might reward you with optimizations in certain cases. But that is optimization, so you are supposed to measure first and only then apply them or have a very clear picture of how your runtime behavior will be.
Probably best to just lift one off of a major library like Lodash, they're well tested and efficient (no need to actually use the library, do include the LICENSE somewhere though):
Why not use the library? With tree shaking and/or direct imports you will ensure the same bundle size as if you just copied the file, and you don't have to worry about licenses etc. In fact, since other dependencies might depend on lodash you can deduplicate the import and actually save on bundle size.
You'll also get notified of any security issues in your lodash imports if your CI pipeline is setup for doing that kind of thing.
Mostly if you look at it out of the context of a single function - a lot of projects end up taking a huge number of dependencies, with a lot of overlapping functionality, because you used one function from this one, another function from that one… I’m fine with using libraries when they actually do heavy lifting that’s core to an application, but a single two line function requiring including hundreds of unrelated irrelevant ones? That will impact the coding style of your team and does have security downsides, like needing to trust the library authors and potentially breaking your build because they changed their APIs or deleted a package or whatever. Copying a 2 line function has very clear boundaries to what it can and can’t do, and doesn’t hide the internals of what you’re doing behind the mystique of “an external dependency”.
If you want to avoid a library then you can do `typeof x === "object" && !Array.isArray(x) && x !== null`. Not ideal, but you can of course turn it into a utility function.
`entry` is a valid Human here, but fails your check. Actually, just creating a new class that implements the Human interface will cause a similar problem, since the constructor will be the class instead of Object. You don't even really want to exclude arrays here:
You're absolutely right. Should have stated that I meant a plain object (key-value-pair). But either didn't want to focus on this topic for that post because it would have been just a bit too much to talk about :D
I really like this package, because it not only uses the method you mentioned for checking types, but is written in TS, so you get the type checking feedback.
Kinda off topic from someone whos mother tongue is not English: what happened in the past years that people apparently forgot how the irrealis works in English? Shouldn't this be "Things I wish I had known when I learned TS" as opposed to "Things I wish I knew RIGHT NOW"?
Even as a former ESL instructor, I'm rather disdainful of statements such as these, as if language is some "well defined codified construct" as opposed to being a fluid means of communication that continually evolves to meet cultural needs.
It's like the word peruse, it traditionally meant "to look over something in great detail", yet you'll find the majority of people use it to mean "to look over some thing in a cursory fashion." So for all intents and purposes, that is the new de facto definition of the word.
As a native English speaker, I never learned formally about irrealis, so it's not possible for me to have forgotten them :P "Things I wish I had known" and "Things I wish I knew" both sound right to me.
Nice article, I like it. I can recommend "Programming TypeScript" by Boris Cherny (O'Reilly, 2019) and "Effective TypeScript" by Dan Vanderkam (O'Reilly, 2019), if you want to learn more like this.
I am highly interested in type programming lately, exactly the kind of thing you're doing here. Do you have any other resources that inspired you? Here are some of my favorites:
I haven't used typescript much but it's surprising to me that you can pass a `const` value to as an argument which is not `ReadOnly`. Does `const` not really mean anything?
That’s a JavaScript specific nuance that typescript inherits.
‘const’ declares a variable with an immutable reference, not an immutable value. If you’re referencing a simple literal like a string or a number that’s effectively the same thing but for objects (and arrays under the hood of JavaScript are fancy objects) while the reference to your given object is constant, the properties of that objects are still mutable.
Same as Java’s final, isn’t it. You can use final when declaring a reference to an object but that doesn’t make the object itself immutable, just the reference itself.
Same in a lot of programming languages. A common source of errors too. Maybe it is a bit more confusing in JS simply cause people have been taught to make an active choice between var, let and const.
`const` means a constant pointer, but doesn't guarantee that the data it points to will remain constant.
In practice, functions, numbers, bigint, symbols, booleans, strings, regex literals, null, and undefined are all immutable. Since you can't change the value, a const to one of these guarantees the value will never be modified.
Objects, arrays (actually just objects with a different constructor), maps, sets, TypedArrays (real arrays), etc are different. You can be guaranteed that you will be pointing to the same object instance because there's no way to swap out data at a location in memory like there is in low-level languages (yay GCs). The entries inside the hashmap or array can be modified though.
Calling `Object.freeze()` will lock down an array or object with some caveats. Sub-objects will still be modifiable (though you could recursively freeze) and this doesn't work for Map or Set (their properties like get/set/forEach will be frozen, but not the actual data) and it will throw if used on a typed array.
You don't have to suffer (subjectively) bad typography. Cascading style sheets were designed with the ability to override author styles with user styles, your Web browser has settings for this.
For me personally, I found that they help reduce the noise in the code.
I also noticed that it makes it a bit easier to "read" the code (not just visually, but "semantically" if that makes sense). As in, I think I have to spend less time "parsing" ≤ than =<, but I don't have a way of really "proving" it.
However, I am mildly dyslexic, so that might play a role in it.
That's all well and good for your personal environment. But I think it's a little crazy for a blog post that's supposed to be teaching things to beginners. "≤" is actually a different string than "<=". I think it's really misleading to render one series of characters as if it were another. For instance, julia actually supports ≤. There are others. On top of that, I don't expect a font to be able to correctly parse code. Sometimes "<=" happens in contexts other than "less than or equal".
Is there ever a reason to use interface over type? From what I’ve seen it looks like they can both do the same thing but with slight differences in syntax
“Type aliases and interfaces are very similar, and in many cases you can choose between them freely. Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.”
…
“For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use interface until you need to use features from type.”
https://www.typescriptlang.org/docs/handbook/2/everyday-type...
That's creating a new type B. In contrast, interfaces can have their definition spread across multiple code units.
interface X {
x(): void;
}
interface X {
y(): void;
}
class Y implements X {
x(): void {
console.log("hello");
}
y(): void {
console.log("world");
}
}
const z = new Y();
z.x();
z.y();
This is important for keeping up with API changes in browsers that may happen faster than the DefinitelyTyped project can keep up.
- in error reporting: type aliases may be replaced by their definition in error reporting.
- you cannot create union types with interfaces
- legacy versions of TypeScript does not enable to create recursive type aliases such as type `List<V> = {v: V, right: List<V> | undefined }`
- interfaces with same name are merged
However the frontier between type aliases and interfaces seems more and more blur. Generally interfaces are encouraged over type aliases. I personally prefer type aliases because there are more capable and seems more elegant to me. However their poor support in error reporting makes me rely more on interfaces when possible.
They are very similar, but they are subtly different in ways that might matter, depending on what you want to do.
A type is statically-"tagged" data from the typechecker's point of view. Even if `Foo` and `Bar` are two types with the exact same fields, the typechecker won't let you use as Foo as a Bar or vise-versa unless you've explicitly declared that Foos are Bars (via type aliasing or inheritance).
An interface declares a whole category of types that are equivalent: anything with the same "shape" as the interface will count as the interface. So you can pass objects, child objects, objects with additional fields attached, etc. to an interface input; if the thing has the fields the interface cares about, it'll accept it.
Which you want to use depends on what precisely you intend to do, but interfaces are handy in TypeScript where they may be less useful in some other languages because the underlying JavaScript is so "duck-typed" and sloppy on what it means for something to "have a type;" interfaces often model more accurately the behavior of "native" JavaScript functions (that will take an argument, assume it's an object, and just start touching some fields on it without caring whether more fields exist or not).
One detail I stumbled upon the other day is that interfaces can use the `this` keyword to refer to the type implementing the interface. This isn't supported for types afaik.
I prefer types over interfaces because of one simple difference: if you use vs code and hover over type alias, it expands it and shows everything inside, while for interfaces it just shows the name.
Short comment about the 'unknown' type: TypeScript 4.4 had me learning about it very recently, because 'unknown' has been made the default type in 'catch (error)' clauses [0]. So most of our code in 'catch' blocks suddenly didn't compile any more after an unsuspecting update of the TS version.
Which is a good thing, because in reviewing those I found several places where incorrect assumptions were being made about the type of error that would be caught.
(side note: TypeScript does not follow SemVer; they just promise to avoid breaking changes in Patch updates, but don't promise anything won't break in Minor updates [1])
We enjoyed this post so much we turned it into an interactive tutorial, you can take the tutorial for free without signing up here: https://codeamigo.dev/lessons/start/96
I think the unknown example is good but also somewhat confusing, because implicit typing would understand what set of types could be in that array at that moment.
Is there another example someone could give for unknown which isn't handled by implicit typing?
... this is the most common way I see unknown used. Then when you fetch data from the server, you are reminded by the compiler that you should do some duck-type checking on it to make sure it's shaped correctly (since responses from a server can be any shape; is it a 200 with your data, or did a caching layer vend you an old version of this data structure, or is something catastrophically wrong and you're seeing a 200 where the payload is HTML saying "Set up your apache server," etc.)
BTW, TypeScript has another useful tool for tying the runtime typing and static typing together: type guards.
function isUserRecord(x: unknown): x is UserRecord {
return (x as UserRecord).name !== undefined;
}
This is a boolean function but the type system understands that in codepaths where it returns true, the 'x' argument is known to have the UserRecord type. Great for codifying your type-discernment logic.
This is a nitpick; but the first example doesn't make sense once we use ReadOnly, because it doesn't return the copied+sorted array. So in practice the final version of sortNumbers would have no effect afaik.
function sortNumbers(array: Readonly<Array<number>>) {
return [...array].sort((a, b) => a - b)
}
And sort sorts in place and then returns the sorted array[0]. So here the newly created [...array] is sorted and then returned by sort and then by the return at the start of that line.
`any` is one of the main reasons why typescript is now so popular. Its a dev-conversion tool. It's amazing for teams that aren't ready to make the full leap. But later migration to strict mode is necessary.
Then turn it off with your compiler options. Typescript never would have gained significant adoption without it. It's essential for gradual conversions from js codebases.
Does Typescript get better? I've been forced to use it for my most recent project, but haven't been given any time to read through the documentation. I've found that I'm spending about 99.9999% of my development time trying to figure out how to get my IDE to not show that their are typescript problem. At this point I hate typescript with the burning fury of a trillion suns. I wonder if this is everyone else's experience?
Edit: So I take it from the downvotes that everyone else is a genius and I'm the only one that has had a problem learning TS.
My experience is that the worst part is the config file. Its documentation isn't great and it's got a lot of cruft and overlapping functionality that can be confusing. It's easy to end up following a guide that's outdated or simply wrong in some subtle way that just happened to work for the original author but was still incorrect, when it comes to the config file.
Once it's set up, though, it's wonderful. No more refreshing only to discover you typo'd something or left out a step. You can hand your code to someone else—or to your future self—and they can often just start using it without having to ask questions, read documentation, or read the code to figure out what it does, because the types convey a ton of info and their editor/IDE presents it to them as-needed.
It lets you get your ideas about how the code works out "onto the page", as it were, and in a format in which a machine can automate most of the looking-up and finding-relevant-information-for-this-context parts. It slows down some code-writing a little (mostly by making you document things that should probably be documented anyway, either with tests [yes, tests are documentation] or in a manual or whatever) but speeds up using code so much that it more than makes up for the cost.
It's an incredible communication tool.
If you rarely need to communicate with others or with your future self about code you're writing, then it may not be worth using. So, if you're on a smallish solo project that you don't intend to stop working on until you're done working on it forever, never plan to hand to anyone else, and that you spend so much time working on that the whole thing's in your head nearly all the time, it might be fine to just write JS.
That was my experience too and yes, it gets better. I went from "I don't understand it" to "I hate it with a passion and want a proper typed language" to "uh okay it's caught 2 bugs I overlooked. This is useful" during my first project.
My advice: accept it's completely alien and is going to take effort to learn. I found spending a weekend sitting down and reading a theoretical guide was useful; there's good recommendations further up this page. Understand there's different levels of applying TS: one of its maintainers told me to start off slow and use `any` wherever I didn't know what to put. I pooh-poohed that because everything should be typed in a typed language but he was right. I have a few `any`s and a few `x as Type` where the code can't work it out because something's gone wrong. It works and my next project will be better.
I still have no idea (and haven't found any tutorials) on how to type form data (DTOs) or any object where stuff is structured but can be optional or required. How do you validate the type definition? What about HTTP responses which might have data in multiple structures - how do you type each of these depending on what's received? All answers welcome.
I will get there. And so will you. Onwards!
Aside: I got roundly mocked on a JavaScript framework's discord asking questions about edge cases to help me build a mental model of the language. Apparently it's "b.shit" and "questionable" and "weird way to learn the language". Well... now I know them I can infer what's going on, understand the workarounds used and why things don't translate from other languages. If that's the way you learn, embrace it. I still think JS (and TS by extension) are weird in places. Neither would be my choice of language, but I'm coming to appreciate them.
Once your environment is configured and you're writing good types, the only errors you see are when you do something that violated your type rules - like calling myRecord.names gives you an error "Property 'names' does not exist on type 'PlayerRecord'. Did you mean 'name?'"
I don't know if the issues you're having is bad setup, or type system abuse. Try a typescript forum or chat group?
I'll admit that, coming from C++, I'm 100% bought in on the usefulness of type systems; but I can't imagine refactoring or making major changes in a project written in untyped JS.
Yes, of course. But that’s the case for all new languages. You need to learn it. After some time you will be much faster in programming with a typed language as in a untyped.
Typescript is very easy, super powerful and lovely. If you are having problems is because there is something you are still not getting, remember is just Javascript but with a super set of tools that make it beautiful
So you didn’t read the (very easy to read) documentation and then complain that you can’t figure out how to use it? And then you wonder why people are downvoting you?
Well you have been told wrong. However it only takes about an hour to read the type docs. So take the time why not? Isn’t that better than being frustrated?
Your original question of "does typescript get better?" can be rephrased as "does any tool become easier to wield with greater/better understanding?" and the answer is "yes".
Without a [concrete] example of what type of issues you faced, I doubt anyone can give you actionable advice, though.
> My JS code has no issues.
Either the types were wrong, or the code was wrong, either way there is an issue. Since you said you're inexperienced with typescript, it's possible the types were erong.
There are certain JavaScript patterns where TypeScript can be a bit inexpressive/unergonomic (especially with a lot of "metaprogramming" or "runtime polymorphism"[1] involved).
[1]: Most of the cases in our company the difficulty of expressing some types of "runtime polymorphism" was what I call thoughtless polymorphism (ie, it usually hides runtime errors that you're unaware of, especially if there's a proliferation of `anys`s in the code)
Everything that TS does JS libraries do better without the horrible tradeoffs ... sadly MS has invested so much into promoting it that it's now almost a requirement for all software development.
Not OP but I do tend to avoid TS. I don't like the additional friction of working with the language (transpiling, unable to copy/paste directly into an interpreter). I also feel like the community at large writes awful baroque code that makes me want to die. Why use a function when 18 classes subclassing eachother across 4 files will do? If you're familiar with the tiktoker @khaby.lame, TS feels like exactly the over-complicated life hacks he mocks.
I've found the perfect middleground is typescript checking with JSDoc syntax. The code is just JS, no emitting necessary, but you get all the type checking.
Together with testing of @example stanzas, you get everything for cheap-ish.
Alas, there is no good JSDoc @example test runner¹. This would be very valuable. If there were, I would let that handle my unit tests and focus only on integration testing.
Edit: runtime type checking at the boundary is also valuable! I've tried runtypes, but actually prefer compiling the ts definitions to JSON Schema with typescript-json-schema, and checking with plain JSON Schema validation.
¹I've used @supabase/doctest-js and jsdoctest, and found both lacking. If you know of a better one, please share!
TS !== crazy OOP.
For some reason, there are people who just want to use Java style OOP in TS. Nest.js is one library heavily promoting that. I can't digest such codebases, they are the definition of over-engineering to me.
Fortunately, out of dozens of medium to big TS projects I worked on, only one used that style. In all the other projects there were very few if any classes.
Fair, I should have been more clear when I said "interpreter" what I really meant is "the chrome debugger" which is one of JS/Node's absolute killer features. I believe support there is on it's way, though I very much doubt it'll convince me of TS' value.
I used to be much more bullish on TS, I like the idea of strong typing in general but the more I use TS the more I feel like it's just the worst of both worlds. I much prefer my types to be deeply embedded in the language design, types as varnish don't make sense to me anymore.
Oh, in the console? Probably won't work? I'm saying that you can run chrome's debugger on source mapped TS files, which is really really nice for bug-hunting and developing locally
But I mean if the biggest complaint you have is that it doesn't run in a browsers REPL console, that feels pretty minor to me. You can easily find TS REPLs all over the web
Right, but the actual benefit comes from having the REPL and the debugger in the same place. Plus the chrome REPL is fantastic, so suggesting I just use a different one is sorta like saying "you can just use your phone" when my default is a Hasselblad. When I'm writing JS/Node I can write code in the REPL in the context within which it will execute, it really cuts down on how much I have to hold in my head at one time because I can just ask the computer. It also makes exploring much much faster because my iteration times are as fast as the computer can run my code. I find it's a much more natural way of programming and about as close to SLIME as I can get while still writing in a language that has easy economic value.
Also for me, the benefits of a statically typed language on a large team heavily outweighs not having TS in a chrome REPL. It's not even close. Maybe your use case is different, but for me it seems like you're missing the forest for the trees.
> benefits of a statically typed language on a large team
I mean yeah, sure, if you have to work on a large team in the browser/node you're going to have to make tradeoffs for that, and using TS seems like it'll help everyone go home at 5. I don't think I'm missing the forest for the trees, we're talking past each-other.
To illustrate a bit further, while I like Rust a lot, (the problems it tackles are MUCH more real than the ones TS does) and I put in the effort to learn and use it on a few personal projects I still find myself reaching for C almost universally these days. Even in the case of Rust's very useful tradeoffs I feel like they cost too much of my freedom, and TS' guarantees are much more surface level for a similar cost.
No one using TS claims JS is broken. People want a good type system in javascript, and with that obviously comes a new layer of complexity
The tradeoff is clearly worth it for certain use cases, and clearly not worth it for others. It seems you are discounting and ignoring cases when it is worth it.
I don't think there are no use cases, but I think they are rarer than people think, and that for the most of development TS will be picked because it's someone preference not because of its actual benefits.
For example if you want prop types you use propTypes. In React TS replaces good error handling for horrible obscure errors and slows down development considerably etc. etc.
propTypes does runtime type checking, which is a different kettle of fish from static type checking.
The advantage to static type checking is that it removes the performance cost of runtime type checking where it's unnecessary; the language's rules make it impossible to build some constructs where the wrong types get mashed together. The tradeoff is that you have to code so the wrong types don't get mashed together (which is, arguably, your goal in the first place).
You can do everything a statically-typed language does in a non-statically-typed language via best practices, but that's a bit like saying you can do everything a compiled language does in assembly via emulating what the compiler would output. In theory, the compiler is saving you the headache of doing that (but depending on the size of what you're trying to write, sometimes it is simpler to write it in JavaScript and skip the type safety. That code is harder to grow, but not all code grows!).
This is a good response to drill down on, because the poster's assessment of the purpose is accurate.
Yes, much of both TypeScript and React are there to minimize performance costs and improve software reliability in tens-of-thousands-of-lines-of-code projects: TypeScript is using static type safety to replace the need for dynamic typechecking (decreasing the expected runtime error rate and the runtime cost of dynamic type analysis; static typing tells you both when you must runtime-coerce types and when such coercion is unnecessary and would waste performance). React is using delta-detection of lighter-weight objects to determine when heavier-weight objects in a declarative user interface API need to be changed, impacting performance (because a handful of equality compares against objects or plain data is a fraction of the cost of repainting every pixel in a table with pixels representing the exact same information as before to the end user).
These are problems people face, but if they are not the problems you're facing, they might not be the tools you need. Not everyone is writing the Facebook UI. There are lighter-weight tools out there that solve similar problems with less complexity (the tradeoff, perhaps, being that if you do find your software needing to scale to handle updates to represent complex, heterogeneous data or infinite streams of information, those tools might not scale easily... But how many people actually have that problem?).
"Use the right tool for the job" is one of the cornerstones of the art of software engineering.
Yes, the right tool for the job, but because TS covers a set of tools its rare that this is exactly the tool you need. I'm sure there are cases but mostly I think people just use it because of the hype/preference and I really hate to make prototypes with TS.
Yeah, prototypes are probably the wrong use case for React or TypeScript because they can crash without causing someone $X million in revenue (by definition; if it can cost someone $X million in revenue, it's no longer a prototype and the team maintaining it probably wants stronger guarantees than what native JavaScript provides for proper use of APIs and data handling if they ever want to sleep at night).
I'm really glad proptypes are not used anymore, ts is simply better. It's more flexible, gives stronger guarantees in some cases (PropTypes.func does zero checks for signature, for example), better support in editors, better integration with other libraries, allows typing hooks/context. If you need runtime checks, use io-ts or runtypes.
[0] https://www.typescriptlang.org/docs/handbook/utility-types.h...