Tricks I wish I knew when I learned TypeScript
cstrnt.dev
cstrnt.dev
[0] https://www.typescriptlang.org/docs/handbook/utility-types.h...
I'm surprised I rarely see these in courses/tutorial. These should be like day 1 material.
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 };
Here, ts will complain that b is missing baz.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.
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.
[0]: https://www.typescriptlang.org/play?#code/MYGwhgzhAEAKCmAnCB...
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.
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 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.
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.
> 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 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.
type Ensure
I define a type called Ensure <T, K extends keyof T>
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 : T[U] };
And which types will be the same as in T.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.
EDIT: clarifications and spelling.
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).
EDIT: improved wording
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.
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. "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!
You mean intersection, right?
EDIT: link to docs on intersection types: https://www.typescriptlang.org/docs/handbook/2/objects.html#...
Wouldn't Required suffice here as well for this as opposed to removing the optional qualifier?
Me, reading this example: oh no, those all need to be added to our project's lint rules to make sure no-one uses them.
I never put it in production, partially because of concerns over maintainability but far more because I had no need for it.
This is overkill that will only confuse everyone who isn't the original author.
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.
In short, utility types are awesome!
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.
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.
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.
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.
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.
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.
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.I know dozens of people who tried to understand monads (including myself) and maybe 3 of them succeeded (I do not consider myself one of them).
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.
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.
[2] https://www.typescriptlang.org/docs/handbook/utility-types.h...
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.
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:
[1,2,3].flatMap(a => [a + 1]).flatMap(b => b != 3 ? [b] : [])
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:
[1,2,3].flatMap(a => [a + 1].flatMap(b => b != 3 ? [b] : []))
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.)
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).
Incidentally, Java does have Optional, and codebases which use it consistently are vastly better to work with. https://docs.oracle.com/javase/8/docs/api/java/util/Optional...
I think it’s only really Haskell (and perhaps languages like Idris) that’s super strict on side effects.
In Rust it’s a simple mut annotation, and perhaps a mutex (and you’ll want that in C too of course) if you’re working across threads.
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
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.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.
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.
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.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.
I have only seen null vs undefined lead to 2 things in my experience: mistakes and bikeshedding.
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).
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`.
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 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.
It would kind of make sense in Java, where only object types can be null. But that distinction does not exist in JavaScript.
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.
Not exactly a language design, but an unfortunate reality.
So, yes.
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.
For example: https://www.typescriptlang.org/play?#code/LAKAZgrgdgxgLgSwPZ...
type Human = {
name: string;
age: number;
}
which also enforces value types in the compiler, rather than requiring runtime guards.Internally in the backend, you’d still use the type you just posited.
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.
If I understand you correctly, that's exactly what a Record is underneath
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.
type Tips = Record<string, TipObject>
const tips: Tips = {}
tips["hello"] // TipObject, but you actually get undefined
It's better to define a Dictionary type like so: type Dictionary<K extends string | number | Symbol, V> = Partial<Record<K, V>>
Then to use: type Tips = Dictionary<string, TipObject>
const tips: Tips = {}
tips["hello"] // TipObject | undefined
That way, you're always forced to check for existence, and you never accidentally attempt to access properties on `undefined`. const todos: string[] = [“walk dog”];
todos[123].toUpperCase() // error!
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: type Tips = Record<string, TipObject | undefined> 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.[1] https://www.typescriptlang.org/docs/handbook/2/objects.html#...
[2] https://www.typescriptlang.org/tsconfig#noUncheckedIndexedAc...
// ... 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);
})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
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).
But I digress, the point is in my experience Typescript is very good about catching footguns left around by ECMAScript.
So I'm surprised there isn't some sort of catch for this as written in the article maybe behind a config flag, not by rewriting the definition.
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.
Surely they could have gone with, like, `final` or `immutable` instead..? I'm sure they had their reasons. But seems rough.
interface Person {
[key: AllowedKeys]: unknown
}
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.The TS crew did discuss having a "pedantic" mode, which is stricter than "strict", but I don't think it's on the roadmap any more.
Arrays are objects, so this is expected behaviour.
https://github.com/lodash/lodash/blob/master/isObject.js
But depends on your needs, and you can also attach a typeguard to it.
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.
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.
tl;dr: Object.prototype.toString.call, you most likely to also want to exclude null, RegExp, Function, Date, Number, Boolean & String (not string)
> if(entry && entry.constructor === Object){}
entry = Object.create(null);
entry.name = "Jason";
entry.age = 42;
entry.constructor === Object // false - constructor is undefined.
`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: entry = []
entry['name'] = "Jason";
entry['age'] = 42;
Again, entry is a valid Human here. function isHuman(obj: unknown): obj is Human {
return !!obj &&
typeof obj === 'object' &&
typeof (obj as any).name === 'string' &&
typeof (obj as any).age === 'number';
}
This checks the shape of the object, and returns true if it's a `Human`. This will work for array or objects or classes or anything.its typical to start off with `var &&` to avoid operations on null & undefined values
!!(x && x.__proto__ === [].__proto__)
any => Object.prototype.toString.call(any).slice(8, -1)
typeName = Object.prototype.toString.call
[0]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Object.getOwnPropertyDescriptors(Array.isArray)
/*
{
'0': {
value: 'https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray',
writable: true,
enumerable: true,
configurable: true
},
length: { value: 1, writable: false, enumerable: false, configurable: true },
name: {
value: 'isArray',
writable: false,
enumerable: false,
configurable: true
}
}
*/ Array.isArray[0] = urlString 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.[0] https://www.typescriptlang.org/docs/handbook/advanced-types....
"What is object? I don't know. Does it meet these conditions? Yes then object is Human else not."
Which libraries do you recommend? I've had to do this a couple of times in my current project and it's painful (and I made mistakes).
[0] https://github.com/arcanis/typanion
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).
[0] https://smile.amazon.com/Effective-TypeScript-Specific-Ways-...
It would be neat to title blog posts in mock Elizabethan English though:
“Miscellania of Out-Most Significance to the Young Man Who Desireth to Learn the Merveillous Type-Scripte”
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.
https://github.com/iiTzEddyGG/typing-euler/blob/master/src/p...
https://gist.github.com/hediet/63f4844acf5ac330804801084f87a...
https://github.com/codemix/ts-sql
https://github.com/jamiebuilds/json-parser-in-typescript-ver...
https://gist.github.com/acutmore/9d2ce837f019608f26ff54e0b1c...
‘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.
const foo = [];
foo.push(1,2,3);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.
* {
font-variant-ligatures: none !important;
}
I cannot comprehend how ligatures ever got popular in programming, particularly in blogs supposedly trying to teach new languages or concepts.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.
https://www.typescriptlang.org/docs/handbook/advanced-types....
- 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.
“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...
type A = { id: number }
type B = A & { name: string }
const b: B = { id: 0, name: 'foo' }
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.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).
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])
[0]: https://devblogs.microsoft.com/typescript/announcing-typescr...
[1]: https://github.com/microsoft/TypeScript/issues/14116#issueco...
https://www.typescriptlang.org/docs/handbook/2/indexed-acces...
Is there another example someone could give for unknown which isn't handled by implicit typing?
interface ServerResponse {
data: unknown;
}
... 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. 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.[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
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.
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)
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.
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.
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.
Which ones do you need to do everything TS does?
> without the horrible tradeoffs
Which tradeoffs are horrible?
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!).
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.
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.
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.
Static typing can be a very useful tool especially when the language is designed around it, in TS it is a painful kludge.
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
Edit: if you are asking about chrome debugger, here's a link to some info https://stackoverflow.com/questions/43627243/using-chrome-to...
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.
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.
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!