All I want for Christmas is these seven TypeScript improvements
effectivetypescript.com
effectivetypescript.com
Okay and maybe I also want it to be aware of type assertions that happened outside the array functions. You should already know that x cannot be undefined anymore. I tested that. I know you don’t think you can be sure you’re actually being called after the assertion, but you are! It’s obvious!
Oookay and maybe just one more (sorry Santa!): I want a ‘pure’ keyword. Any function that is pure can only call functions that are also keyworded as pure. No side effects, guaranteed.
https://code.lol/post/programming/higher-kinded-types/
I have to admit it's a bit heavy, but this is totally representible in today's Typescript if I understand your ask correctly. It would be great if it was a more obvious / automatic inference, though.
Or maybe, you could pass some kind of "scope" interface that provides functions for executing side effects. So "impure(IO) function printStuff()" brings all the functions in IO into scope for the body of printStuff.
More likely it would lead to most functions looking like they are pure, whether they are actually pure or not. For example, any function written before the "impure" modifier was introduced would look pure.
Still, it feels dangerous to be able to feed a generic object method in and expect the return to be typed based on a previous typing of the composite instances. Or maybe it feels like something that should always pass a compile check - because Object.entries can always be any - and only fail at runtime. Even if it was a feature, it's something I wouldn't implicitly trust a compiler to catch all edge cases of.
Not really. Typescript already has the
keyof
keyword. For some reason though Object.entries(T)
returns string[]
when it could easily return keyof typeof T
I've had to write a helper function that essentially does this for you with a generic and a cast, but it feels unnecessary.It's better to default to the best practice. What you should wish is for an `impure` keyword and then all impure functions that are not marked as such would raise a compiler warning.
If you want something like this you might as well use a completely different language.
But one solution might be a “noImplicitImpure” option.
If pure functions were explicitly marked as "pure" the compiler could just conservatively assume all other functions were impure. But the opposite does not work.
This is correct behavior though, and what you propose would be wrong. The type isn’t narrowed. Type U satisfies type T when the keys of U are a superset of the keys of T, so long as the keys map to the correct value-types in the intersection of (keyof T) and (keyof U)
This is because the only time extra keys are outlawed is when you instantiate an object of type T and explicitly declare it as a T. I can link if you need but this is an important subtlety which is why Object.entries returns string instead of keyof T, and it’s not borne of MS/TS developer laziness
Could they add a strictness flag that lets objects actually be type safe and enables related functions to work out of the box? For me this stuff is a regular pain point as you end up typecasting at every call site :(
It would be incredibly painful to write idiomatic JS with constant narrowing required and would lead to huge performance issues.
Imagine you write some generic sort capabilities, sorting by label, createdDate, etc., and then you declare some types
interface SortableByLabel { label: string }
interface SortableByCreatedDate { createdDate: Date }
...
Then you implement a method function sortByLabel(a: SortableByLabel, b: SortableByLabel) { ... }
I've written enough, I think you can see how easily this works today and how painful and un-performant it would be to do this if you had to narrow your objects or constantly write extra info into your types to allow them to not enforce narrowing, and then there's a whole can of worms around your "widen"-able object types being illegal to functions that require exact types, ...Instead you can just write arrays...
interface MyInterface {
key1: Type1;
key2: Type2;
...
}
const myKeys:Array<keyof MyInterface> = [
'key1',
'key2',
...
] as const;
function operatesOnKeys(object: MyInterface) {
// don't use Object.entries...
myKeys.forEach((key) => {
object[key];
// TS is happy, and bonus:
// this will be type checked later if MyInterface changes
});
}
I think TS has some issues but this isn't one of them.https://www.npmjs.com/package/ts-is-present
that particular case is a small snippet but the package also comes with helpers for reaching into objects to do a similar thing
Pipe dream, unfortunately. If pure functions accept objects as arguments, those objects could have impure getter methods, or perhaps be proxy objects that have impure methods. Pure functions could also receive Function objects, so you'd have to indicate that the passed arguments are pure as well.
Unfortunately, because TypeScript is optional, such a declaration on the functions and their arguments would also be optional, meaning that you couldn't even rely on this as a language feature (your caller could pass you an impure function even if you specified you didn't want it).
Your last argument is confusing: the same applies to everything in typescript. You can also pass in a string to a field that's demanding a number by using "as" so you never have any type guarantees in TS, there is value in it nonetheless.
And the same would surely apply to pure functions (I.e. inpureFunction as PureFunction)()...) It could still be a valuable feature anyway
It's especially bad to do this if you're going to perform optimizations based on those safety guarantees (thankfully, typescript doesn't do that.) You can say 'don't use the pure keyword then' but the fundamental design of JS means that in practice almost no functions can be pure. The runtime has to identify cases on the fly where pure optimizations are actually valid.
Though now I’m doubting myself because maybe it’s possible to use complex enough assertion functions to really prove that. But even if you don’t, it’s still an incredibly useful language. You’re basically saying, if X is true then the rest of this program should fit together like so.”
You're right that in the end once you pull in outside data you're having to make promises that you may not be able to keep - I just think purity is an especially difficult set of promises to keep, and the existence of proxies and getters in JS (along with how they work) makes purity basically impossible, so it's just a footgun waiting to be used. And TS isn't going to optimize your code using it, so it's not really delivering value either.
But imagine that TypeScript, in addition to pure functions, also allows you to tag objects as "POD objects" or "JavaScript objects". The POD ("plain old data") objects can't be proxies or have impure getter methods, and the "pure" keyword actually means "pure if all arguments are POD objects" (this is to avoid having to write N purity overloads for an N-ary function).
You want to call a pure method from your impure method, and pass it one of your arguments. Your arguments now need to be declared to be POD objects, which means they are incompatible with the rest of your code base. This has to go all the way up... it's basically "colored functions" except it's colored objects this time, and the colors are completely incompatible (async functions can call sync functions, but impure functions can't call pure functions with impure arguments, and pure functions can't call impure functions at all).
[0] https://www.typescriptlang.org/play?#code/C4TwDgpgBAkgYgewVA...
I'm aware. That doesn't make it impossible to differentiate as Webstorm shows by coloring the bar differently for fooA vs fooB in your example. But yes, it would have to be implemented as part of that pure feature.
[1,2,undefined,3]
.filter(<T>(x: T|undefined): x is T =>
typeof x !== 'undefined'
);
Yes builtin lang support would be better, but that gets you out of trouble in a pinch, without adding more 5 line npm baggage.N.B. if you are in a JSX file, you'll need to add a comma: `<T,>`
Then rather than marking a function as pure you instead check it is pure by looking at what effects its function type has!
A new syntax for tuples is overkill. TS is already getting quite syntax heavy and I feel that the current methods for defining tuples are more than adequate—either with “as const” as described in this post or like “const foo: [number, boolean] = [5, true]” as the TS docs recommend for tuples.
Runtime type evaluation—please no! This is the perfect domain for libraries to exist in. When you import a library, it’s explicit what’s going to happen at runtime. Libraries are easy to debug, forkable, and swappable. Baking this behavior into the compiler not only foregoes those benefits, but also makes the output less easy to predict and will of course add overhead to the compile.
My own wishlist top 3:
1. Better following of type narrowing through branches and functions. After I filter out all nulls in an array or prove that some optional variable is definitely not undefined, there are still times when ts can’t tell that that’s the case. I can force it to with workarounds, but wish I didn’t have to.
2. Compiler perf improvements. Hard to disagree with you there! Esbuild locally and tsc in ci is okay-ish, but not an ideal state for the ecosystem to stay in for the long term.
3. Consolidation of tooling. For node do you go with tsc and run the output? Or ts-node? Esbuild locally and tsc in prod? Deno? Browser-side, similar questions but throw in Vite and all the other options, too, and even for someone working in this environment regularly, setting up a new project with all the proper tooling is far too much effort.
I actually really agree with the last sentence, and think an officially endorsed set of projects would be pretty useful. The first part of your wishlist though, I don't think is a big issue. I'd say close to 100% of the people using a bundler are using tsc as a linter. Esbuild, babel-typescript, rollup, swc, these all strip TypeScript types and don't raise TypeScript errors at all. Those that aren't using tsc as a lint step are going to be directly running the output of tsc directly, given that the only way to not do this is ts-node, the documentation of which advises using tsc as a linter.
https://code.lol/post/programming/typesafe-function-composit...
It's even more powerful if you restrict yourself to point free semantics - then you can use higher kinded types to do generic-level variadic composition.
As well, optional generics are trivially achievable in TS via default type parameters, unless I'm not understanding that section of the article.
I had a feeling some form of this must've been possible, especially that with newer versions of TS the compiler has become increasingly better at handling recursive types and doesn't throw the dreaded TS2589 error nearly as often.
He links to another post that goes into more detail: https://effectivetypescript.com/2020/12/04/gentips-1-curry/
In particular the question and answer in the comments should make it more clear.
Please remember that these extends keyof and co were included in ts not because it’s so super cool with types. They are there to cover common dynamic patterns of existing js codebases and standards. You don’t have to make same mistakes again, and definitely aren’t expected to make this problem worse. There’s already enough popular projects with perfect multi-page meta-type gibberish instead of plain human-readable types. Which wouldn’t require rust-tsserver running on a supercomputer in the first place.
edit/afterthought: I just do not care about JS-compatibility per se. It's a nice guideline but I'd like the language I use to be as good as possible, because I'm trying to use it to do things, regardless of their ideology.
The correct question is not where our pain points are, but how far these pain points are from what we want to do, i.e. are these points a real pain or just our own perfectionist anxiety. Typing for the sake of typing may not help doing the job before thursday.
In no sense are the issues with TS "perfectionist anxiety". They're real, fixable problems that make it harder to write safe or simple code.
I mean that I don't care about JS compatibility _exactly_. I still want to write JS, just with all the problems... fixed.
I agree with you - while neat types are cool from a mathematical perspective, they impose a huge maintenance burden. Most times, it's much smarter to accept a bit of type unsafety than to try to achieve perfection.
Pretty much the only exception is in library code where you can use advanced types to provide improved developer experience or prevent user error.
tsc isn't just slow, it's SLOW.
% : >file.ts
% time tsc file.ts
tsc file.ts 3.26s user 0.15s system 145% cpu 2.347 total
3.2 seconds is just embarrassing. Nothing is even close to this; every other compiler or interpreter is in the dozens or maybe low-hundreds of ms. clang++ -O2 and rustc are both ~150-200ms, and these are not compilers known for their excellent performance. The startup time of tsc is an order of a magnitude slower than anything else. I didn't measure lines of code/second performance, but with such a slow startup time it's not that important because no matter how small your project: typescript will be ridiculously slow.I don't want to run any "watchers" or special dev servers for the frontend or all of that ridiculously complex circus to work around this embarrassingly slow tooling. I just want to request "/file.ts" and have it serve a compiled version on the fly. This is simple, easy, and even with caching just a few lines of code. It's how everyone was using coffeescript back in the day.
What we have now is millions of lines of code and a world of complexity to work around an embarrassingly slow compiler.
Yes, because I don't want to wait seconds every time I change a typescript file and I don't want to world of complexity to make it have the same performance every other compiler has out of the box. My "solution" for this is to not use typescript. I'd like to, but it's just too painful.
A lot of developers decided slow compiles - I don’t know what pathological case you stumbled on to make a single file take 3s tho - is far superior to needing to suss out runtime errors, and I find it hard to believe you made the choice to write naked JS instead of TS. If you picked a different language, fair enough I suppose, but I have to admit I’m not that sure why someone would tap JS/TS if another language is equally suitable. I am a front end guy after all, so I don’t particularly have a choice
It's an empty file. It's not a "pathological" use case. It's simpler than "hello world". It's literally the simplest possible use case. I do have to say I have a fairly slow laptop, but everything else is plenty fast enough.
> I find it hard to believe you made the choice to write naked JS instead of TS
Why is this hard to believe? Most things I work on are a few hundred lines, maybe 2,000 lines at the most. Not huge, but not trivial either. Some of these are personal projects, on some I work as a contractor. Many have an uneven pace of development: lots for a bit, then nothing for months or years. I want stuff to "just work", even after 2, 3, or more years, and I don't want to update loads of tooling every time I come back to it. This is already a bit of an issue in JS-world in general, so I try to keep things simple. This means, among other things, simple build steps. With a okay-ish performance (doesn't even need to be "fast") you can keep builds very simple. With the current performance this is a lot harder and you need a lot more complexity to make it work smoothly.
The fact that you need to "setup" anything is already a problem. I want stuff to Just Work. No extra processes. No extra steps. Nothing to update. Works today, works in five years. Just start one process and it will work. I don't understand what's so hard to grasp about that.
This is trivial to do with a compiler that has an okay-ish (not even "fast") performance. It's not feasible with TypeScript today, even for fairly simple applications with a few hundred lines of code.
It's totally reasonable to keep your setup simple, all that complexity is often not required. It only really pays off on larger projects (it's super simple to set up nowadays though).
The type checking usually only happens during development time, continually or on demand. Shifting type checking to the IDE is how Vite can be so fast.
I don't know if there's a popular solution for immediately seeing changes in production like you seem to be looking for (running dev servers in prod is probably not a good idea), but regardless I think Vite is worth a look. It gets rid of a lot of the cruft people criticize about web development.
No, I just want to run it in dev. There is no good way to do this currently because it's just too slow to be smooth.
Vite is undoubtedly an improvement over what came before, but the entire method of operation is just not what I want. I don't want to run a Vite process in a second terminal. I don't even use npm for half the things I work on, because there is no need for it. I just want to compile a file every time I load it on my dev machine (i.e. on 'GET /file.ts' runs 'if (hash("file.ts") != cached_hash("file.ts") { tsc("file.ts", "file.js"); serve("file.js") }'). For production, I just compile the files in the binary and serve those.
There are other reasons one might want to use Vite ("hot module replacement" and such, although I personally dislike it) but currently you need to use Vite or something like it, with no realistic option to keep it simple if you want to. If tsc was faster it will become realistic to make everything a lot simpler, with a lot less tooling, and a lot less complexity.
> other, arguably better workarounds than not using Typescript
My current plan is to look at rescript when I have some time. It seems a lot faster and offers mostly the same features. Though the big advantage TypeScript has is that it's JS-compatible and "upgrading" a project is a lot easier, and that's it's kind-of standard whereas rescript is kind-of obscure (which doesn't matter for my personal projects, but does for some things I work on as a contractor, where I want to make sure the next person can work on it in 2 years after I'm gone).
He has a point, although the advantages outweigh the disadvantages for sure.
Use swc or esbuild if you want fast TS compile times and use your IDE or eslint for type checking during development
Node is fast but there is an initial startup penalty, especially if the project has many files to load at runtime for interpretation. I bet it's spending a lot of time on require().
Faster than tsc because Ruby doesn't need several seconds to start up and neither does node. Opal translates Ruby to JavaScript (and is implemented in Ruby) so we don't need to engage in unfounded speculation about non-existing implementations:
% :>a.rb
% time ./bin/opal --compile a.rb > a.js
./bin/opal --compile a.rb > a.js 0.54s user 0.12s system 90% cpu 0.735 total
It's not very fast, but still several times faster than TypeScript, and it includes a large runtime (the resulting file is 2M); disabling that speeds it up by ~150ms (most of that seems to be in the source map generation for the runtime, since --no-source-map with the runtime gives similar results). Opal is also a fairly small not-very-active project developed by a few people in their spare time, not a major language sponsored by a major company used throughout the world.Coffeescript, which uses Node, also doesn't support from multi-second startup times:
% :>a.coffee
% coffee a.coffee
% coffee a.coffee 0.10s user 0.03s system 101% cpu 0.125 total
> especially if the project has many files to load at runtime for interpretation. I bet it's spending a lot of time on require().How often do I need to repeat it? It's a single empty file. Are people even reading what I'm saying or what because I've had to repeat this more than once now. It's a a file with zero bytes. No content in the file. No diskspace is used. The number of bits in the file is zero. A single empty file.
> especially if the project has many files to load at runtime for interpretation. I bet it's spending a lot of time on require().
I am talking about tsc itself. If there are hundreds/thousands of modules to load, node will spend time on that. coffeescript is a much smaller transpiler, with just a handful of source files.
EDIT - looks like tsc might get distributed as a single file, which helps here. However, it is a 7mb source file :)
https://raw.githubusercontent.com/microsoft/TypeScript/main/...
When I execute it locally, with no arguments, it finishes instantly as far as I can tell. So maybe tsc itself is indeed super slow for just an empty file.
CoffeeScript simply compiles code and does not perform static analysis
tsc is implemented in JS and performs static analysis which makes it (unsurprisingly) slow. I suppose you can make the argument that tsc should have an early exit for empty files but that's just unnecessary bloat to the code base.
Surprisingly, tsc doesn't have an option to skip type checking [1]. The best you can do is run using --skipLibCheck to skip checking node_modules.
On the other hand Ezno a semi compatible TypeScript compiler can use a more low level binary format which is only one pass and whose name references are already resolved https://twitter.com/kaleidawave/status/1596445852918325250?s... . This is only really possible because it is written in Rust so low level byte and memory management is relatively simple
const pt: [number, number] = [1, 2]
As for piping functions... that's just how functions work. Not sure what the point of that piping syntactic sugar is.
If I’m solving a math equation, I know that I need to follow the order of operations—I don’t need to rewrite the equation in an entirely new way. It’s more intuitive for me, at least, to wrap my head around the current approach.
The pipe came to R a few years back and I would argue that it is one of the best features. I admit that data munging is a very stepwise process but I find myself frequently missing it in Ts when I code. It makes the code denser as you use fewer intermediate variables while the code id still easy to debug.
const plus = (a: number, b: number) => a + b;
pipe(5)
.thru(plus, 3) // Pipe(8)
// ^---- this is inferred as `number`
But this doesn't function plus<T extends string|number>(a: T, b: T) {/* ... */}
pipe("five")
.thru(plus, "three") // Pipe("fivethree")
// ^--- this isnt inferred or narrowed,becoms `unknown` iirc function doSth<A, B>(a: A, b: B): B { /* ... */ }
function callFn<Args extends any[], Out>(fn: (...args: Args) => Out, ...args: Args): Out { /* ... */ }
const foo = callFn(doSth, 'asdf', 'bsd' as const) // a: 'bsd'
const bar = callFn(doSth, 'asdf') // errorI thought about implementing it myself (maybe as a babel transformation) but I couldn't figure out a way to access the type information.
https://en.wikipedia.org/wiki/All_I_Want_for_Christmas_Is_My...
Extra funny because I’ve been caught noticing myself using english plurals in my native language. (so for 'lantaarnpaal' the plural would be 'lantaarnpalen' but I’ve been saying 'lantaarnpaals')
I guess it might just be how language evolves, it’s interesting to see it happen. I wonder if grammar books will ever adjust to these changes?
Does English have a special rule? English.SE doesn’t seem to have a clear answer for this.
What's the rule for cases where it is not known whether X is singular or plural ? Phrases like "What we found" or "All I want for Christmas", for example. French grammar has several rules where, if a word must agree with a still unknown number or gender, then singular masculine is used: "J'ai mangé une pomme" vs. "La pomme que j'ai mangée" (the verb agrees with the apple only if the apple appears before it in the sentence).
EDIT: I just realized that "All I want for Christmas" may actually be a plural, but "All I want for Christmas are a cookie" sounds strange...
Hrmph: https://github.com/microsoft/TypeScript/issues/364 - https://github.com/microsoft/TypeScript/issues/4895
There are few exceptions (namespaces, enums) but typescript has been quite averse to adding non-standard language level features outside type annotations. This feature will likely come to ts after this proposal has been accepted for js.
- Type the default export in a single expression (issue #13626, open for 5 years)
- Negated types. E.g. `string & not 'abc'`. `Exclude` only works for union types
- Allow 'never' to take a type parameter that will show up in my IDE when a type resolves to it. 'never' is often used in more complex type construction, and if something is unexpectedly resolving to 'never' it would be nice to know which 'never' it is, a bit like an error message
I feel it in rust when current error spot jump wildly across all of my code as I'm fiddling trying to fix it or modify it. It's not pleasant, but funny when you fix the last few red spots in one place only causing the huge red bloom in far away place or red jumping back and forth between two far away places as you type.
Writing Clojure I really liked that there was a baseline assumption that functions are pure and a clear signal in code when it’s stateful. It doesn’t have to color functions (but ahem it works great for Haskell), but it seems like a reasonable opt-in default that shouldn’t be so painful.
I find it amusing/sad that there is simultaneously reticence in adopting Elm and an eagerness to gradually copy all of its features as the JS crowd learn that typed pure FP isn't such a bad idea after all.
Fixing very slow intellisense in VSCode when large numbers of types are imported. Such as from AWS SDK or CDK.
Show resolved types ie {...A, ...B} vs A & B.. There are hacky workarounds that shouldn't be necessary.
Issues open for years on both.
Or I'm misunderstanding the question. (In which case, please, somebody correct me.)
But if I'm not... Elm, Haskell, F#, and probably most other FP langs support this.
What I specifically meant by "(A -> B, B -> C)" was "two arguments, each of which are unary functions, and the type of the argument accepted by the second function is the same as the the return type of the first function." I think in formal notation, the full signature would be "(a -> b) -> (b -> c) -> (a -> c)" but I'm not overly familiar with that notation so I may have gotten that wrong.
The goal here is to be able to describe a function as mentioned in the article, that can accept any number of functions (either as varargs or a single list of functions), and ensure that the input of each provided function has the same type as the output of the function that precedes it.
data AlignedFunctionList a b where
Identity :: AlignedFunctionList x x
Pipe :: (x -> y) -> AlignedFunctionList y z -> AlignedFunctionList x z
pipe :: AlignedFunctionList a b -> (a -> b)
pipe Identity a = a
pipe (Pipe f g) a = pipe g (f a) (&) :: a -> (a -> b) -> bSo, `(a -> b) -> (b -> c) -> (c -> d) -> ... -> a -> z`.
The key is mapping through all of the arguments and enforcing that argument N's output is a subtype of argument N+1's input.
https://stackoverflow.com/questions/35959979/functional-comp...
let x |> f = f xI have tried to use a couple of tools with better types on the job and on the "productive/industry" side of the spectrum. That consistently led to disappointment. It's been a while since I gave up on TS; what drove me away then was the typings I had to download and update and debug.
Nowadays I'm working on learning the purescript terrain for frontend development. In that world, the limitation is not the language but my ignorance. That feels better.
It is the nature of programming that there are tools that solve these problems. Tools that aren’t TypeScript but can be used on top of TypeScript. Exactly like TypeScript is to JavaScript.
The `satisfies` syntax from 4.9 was a huge win, and the `const` indicator on generic arguments coming in 5.0 is huge for me.
It can be emulated by giving types an unique property, but this is kind of a hack.
Shouldn't you know without red squiggle if you fixed something?
That's like waiting for the red squiggle in Word to know if you spelled a word correctly.
this works in 9 out of 10 cases and is hugely effective - as long as the ls is fast.
this is like branch prediction. better the faster you can verify that the branch you predicted was the right one.