But then I find some third party code which seems to use the kitchen sink approach to typing, AKA the "look at how smart we are" approach, and it's a nightmare to understand. A type system is supposed to make code easier to work with, by the time you're spending more time understanding the types than the actual code there's something wrong.
In these cases I wish Typescript was more limited and I would gladly give up a lot of the new stuff to enforce simple types across the ecosystem.
That said, I don't really have an opinion on who is at fault here - coders for writing overly elaborate types, or Typescript for allowing them.
Example the new satisfies and some upcomign "as const" features to generics I'm looking forward to
Backwards compatibility is to blame. Typescript was designed to support a number of patterns which was already used in JavaScript.
For example events are registered in the DOM like `element.addEventListener("click", (ev) => ...)`. The type of the callback will depend on the string passed as the first argument, so the type system need to support literal strings as type discriminators which is a weird feature not necessary in most other languages. If the API had been designed with static typing in mind from the beginning, it would have been designed differently and wouldn't need such a complex type system.
foo(get_some_myenum_value(), …)
{type:get_another(), …}
These are as unguessable as if myenum was a string.Magic string are use all over JavaScript for different purposes. For example createElement('P') could just be new PHtmlElement() ...
In other cases magic strings could be replaced with enums.
I was curious how other languages did discriminators in a way that makes Typescript's feel alien. Just having different functions doesn't really tackle that.
type Shape = Square Int | Circle Int
and then pattern matching: print (Square size) = ..
print (Circle radius) = ...
In Typescript this would be something like: type Shape = { type: 'square', size: number } | { type: 'circle', radius: number }
And string matching for type narrowing: function print(shape: Shape) {
if (shape.type === 'square) {
...
} else if (shape.type === 'square) {
...
}
}
The TypeScript approach is comparatively weird and verbose but also pretty clever since it doesn't require any change to the core language.It also have some pitfalls, e.g. you can write:
print({type: 'square', size: 10});
But you cant write: const x = {type: 'square', size: 10};
print(x); <-- type error
You have to write: const x = {type: 'square' as const, size: 10};
print(x);
Things like this just complicates the type system and type inference.Now take mapped types, template literal types, key remapping... Those features aren't necessary to express legacy DOM APIs, this is just for the sake of having a complex typing system. And that's not to add a new feature that unlocks some type magic that wasn't possible before. No, those are just shorthands for things that you could already express in the existing typing system.
For example the 'options' parameter is a common pattern where an object with a set of properties are provided to override a some default behavior. So you need to define a Foo type with a set of properties, and then you need to define a type which is like Foo except all properties are optional.
Some frameworks implement their own flavor of inheritance e.g. through mixins. The type of an object with mixins is not just the sum of all properties, you might also have one property overriding a property of a different type in base mixin, so you are adding and removing properties. You need a really powerful type system to be able to represent this.
This may be true in a lot of cases, but certainly not all.
I’ve spent a ton of time on the types that validate that the input/output schema of my request handler function matches the defined schemas, and that was hell, but now any other of our developers will see delightfully red squiggles when they do something wrong. That’s a force multiplier and absolutely worth the effort.
Also try exploring npm/telegraf classes (auto-documented) by looking at their type definitions. The whole bot api could have been written as simple repeated definitions, but someone chose the clever way of multi-level type mutations and derivatives.
Typescript will soon need secondary community-maintained type contracts that are human-readable and cross-checked with those in libraries. Something like @humane-types/*.
(I say this not as a dig against TypeScript or the way it's been designed; I just think the parent wants it to be something fundamentally different from what it is)
This is the primary argument against worrying about N+1 standards. We know it's bad, if we simply do nothing at all, we know it will get worse. We can and should at least try.
All those legacy features and existing pages are the back pressure that keeps the evolution of the web stable.
It does reach admirably far into checking the regular dynamic-language programming patterns like using string variables to index into structures. Which is where a lot of its complexity comes from, but I wouldn't call that stuff "deranged". JS is a dynamic programming language, and people write it like a dynamic programming language, and typescript decided to meet it where it was at, which is probably the only reason it's gotten so much traction in the first place
TypeScript also supports some really advanced typing I've yet to see in other languages like template literal types (don't @ me, I mostly work with dynamic languages at my current job). I feel like even the TS docs haven't caught up to the full power of TS's full featureset
1. Use an enum
2. Use an intersection with an enum and literally anything else (except a number)
3. Use a private field (only works with classes)
4. Use a `declare const FOO: unique symbol` keyed object intersected with anything else (works with everything including numbers, lots of boilerplate to declare a new nominal type).
In particular iirc you can't use your nominal/branded type as a map key in a type-safe way.
enum A {}
type B = { a: number } & A;
function newB(i:number) {
return { a: i } as B;
}
let b:B = newB(5);
let m:Map<B, string> = new Map();
m.set(b, "test");
m.set({ a: 2 }, "test"); // errors declare const NOMINAL_BRAND: unique symbol;
enum UserIdType {}
type UserId = string & {[NOMINAL_BRAND]: UserIdType}
type UserMap<V> = { [key: UserId]: V };
But at some point in the past few years, this is now supported and things like this work as expected: const userId: UserId = "234" as UserId
const coolUsers: UserMap<boolean> = {}
coolUsers[userId] = true // works
coolUsers["isThisOkay"] = false // failsBut anyway, what I want from TS is
type UserId = unique string; // or something
const userId = "1234" as UserId
and no further boilerplate or trickery to get it to work. Would certainly be nice.But that won't be added because there is no native JavaScript equivalent.
So to differentiate behavior depending of which part of sum type you got you need some value in the "real world" like a tag that can enable you to do that. Because all the type information is gone at runtime.
Strongest violation of this rule I know of are enums that generate their own JS code, but they are their separate thing. Something like sum types would need to be present everywhere.
Honestly I kind of like it this way. I'm trying out Rust right now and I'm very often surprised how what I do in one spot depends on huge context of all the generic types that can create wildly different behavior (mostly huge variety compile type error messages in surprisingly varied and distant spots) depending on what types they infer.
The other issue with the typesystem is the (soon to be obviated) need to compile to decent javascript and interact with it. That made sense before WASM existed and it will make a lot less sense once the component model is stabilized.
Always check ts-essentials for the ideal abstractions built with the wild new features.
Is it safe to say a non-zero portion of "the community" is headed the direction of:
write it in _____ + compile it to WASM + glue DOM stuff to it with some sort of JS bridge?
Just search github.com and you will find countless example repos for many languages, but understanding typescript first is probably a more productive use of most people's time.
If you work in web-tech, I consider avoiding JS and TS to be almost negligent to some extent in 2022.
Say I have `type Foo = 'bar' | 'baz'` and want to validate that an input string is within the set of Foo. Why can't I just enumerate the type to determine the valid values at runtime?
Occasionally I bump in the same things as you when I'd like to have some JS code generated on the basis of TS type information. I think there could be some preprocessing tools that could generate JS validators on the basis of TS code.
Now, trying Rust I strongly appreciate this separation. In Rust what your code DOES might depend on what you are going to do in the future with the result it returns. It's really shocking for me and makes finding errors really hard when the spot of the error messages jumps wildly across large chunk of your code as I change it.
I really prefer TS philosophy of "types are for you and your text editor and the actual code is for runtime".
Is there something inherent with TypeScript that is causing issues that you can point to?