I did a presentation on some of these annoying quirks of TS called "Typescript is a liar.... sometimes" - a play off of one of the greatest 'Its Always Sunny in Philadelphia'. 'Science is a LIAR... sometimes' My previous work place loved it, my current one is.. well... yeah. If you make a full length video please do it in the format of the IASiP scene.
A weird one to me is that defining an interface/type method using the "method syntax" is not exactly the same as defining it using the "field + function type syntax": interface Foo { bar(n: number | string): void; } // Using method syntax, parameters are bivariant const foo: Foo = { bar: (x: string) => { /* ... */ } } foo.bar(10); // I just passed an integer to a function implementation that only takes a string. interface Foo2 { bar: (n: number | string) => void; } // Using field + function syntax, parameters are contravariant const foo2: Foo2 = { bar: (x: string) => { /* ... */ } } // Fails as "it should"
This is the worst design decision they made so far. It was done mostly to keep arrays covariant, which is also bad. Before the introduction of strictFunctionTypes all parameter were bivariant. I hope they will add one more knob to turn this cringe off.
I've been doing front end with typescript for over 7 years now. What triggers me is that for large scale apps, I feel there is no alternative for quite some time. When I go and write something else like a cli tool, web api etc I always feel REJUVENATED by the plethora of choice. And when I want to write a UI, the programming world right now is like .I. to me. Edit: typo
I gave a internal presentation at work about the theorical background behind typescript and the glaring issues it causes. My favorite is creating a never from anything which destroys the type system entirely class Marker {} function castSilently(v: Marker): T { if (v instanceof Marker) throw null; return v; } The argument is reduced to never, but it actually accepts any value, not just instances of Marker
For me t's reduced to either unknown (if nothing is explicitly passed into T, or inferred from variable type), otherwise it's inferred. class Foo {} fits into class Bar {} - the name is irrelevant here. Why? because classes are not really anything! They are syntactic sugar over functions with prototype chains. You need to widen the type of the function by narrowing the type of its parameters. So it would be equivalent of doing function Foo() {} and function Bar() - both are functions, they accept the same parameters and return the same type, therefore they are equivalent. If that was not the case, then it would not be possible to pass functions around. It may seem counterintuitive at first, since you expect classes to behave differently. But this is JavaScript world :)
@@fallenpentagon1579 slightly incorrect. Never is indeed equivalent to the empty set, but that means that never is assignable to anything, not that anything is assignable to never. In fact, NOTHING is assignable to never, there is no value in the set, so no value should ever be assignable to it. Also, no set other than the empty set itself is a subset of the empty set so even without looking at the values it is clear that no other type is assignable to never while never is assignable to any type. So it is correct that a never type can be returned as T. The trick in this snippet happens because the assignability of the parameter uses structural typing and the instanceof check uses nominal typing, and they diverge in this case. So the parameter is a set of all values with that structure (which is all values), the conditional should narrow it to the set of instances of Marker, but since they are under the same name TS considers them to be the same. Since the conditional block is divergent (it throws) then TS assumes that the following statements run on the exclusion type, the difference of the sets, which is never. It is a wrong assumption in this case because of the structural and nominal mix, when it involves just one of them it is perfectly valid. But this IS the intended behavior. Edit: I sent a short answer before from my phone on the road, expanded it more now
This is what made me go OH MY GOD FINALLY when I tried rust a few years back, immutable actually means immutable and you can't just start pushing items into const arrays.
While I think these examples are great, I don’t often (or ever) find myself or the people I work with mutating function arguments and *not returning the result. The only thing I could think of that might still break is if it’s a nested object and someone makes only a shallow copy, performs a mutation, returns the result, and as a by-product, accidentally mutated the function argument and uses that somewhere else.
It's not likely to trip you up when you're consciously mutating it like in the case where you would return it. The scenario that induces hair loss is the one where a consumer of the object decides to mutate it somewhere without visibility. Now you're stuck with OOP shenanigans.
Well yeah, that's how functions work in JS. Classes are just fancy syntactic sugar functions with prototypes. The concept of "constructor" is just using "new" in front of a function call to create a new prototype instance.
If you don't enable "downlevelIteration" in your tsconfig then TS will not let you splat Set objects as if they were arrays. It causes inconsistent behavior esp if you end up calling a JS file that splats a Set because running it with node will work but running it with ts-node will cause the splat to be undefined (if downlevelIteration is not true).
This is because downlevelIteration is a compatibility feature to support splatting in ES3 and ES5. If you want to support "splatting" natively, you should target any ECMAScript higher than ES5. You have to make sure the runtime you run in also supports that feature (ES6+)
you can use nominal types, though it will be a bit clunky type ReadOnlyResult = { readonly hello : string } & {description: "readOnly"} type Result = { hello : string } const toReadOnly = (item) :ReadOnlyResult => item as ReadOnlyResult const test = {hello:'world'} const typedTest = toReadOnly(item) const mutateResult = (item:Result) => { item.hello = 'barf' } // error mutateResult(typedTest)
You can also access an element in typed array like this arr[i] - and it does not recognize, that this can return undefined. While it works fine in for loops where you limit the range of i, in other places you can get a runtime error like that.
To be fair, in the first example its explicitly an (array of [number or string]) not an ([array of number] or [array of string]), so it checks out that adding a number to an array of strings results in an array where the elements are only number or string Yes, still an easy footgun, but in this case its just setting the wrong type and it doing what's expected of that type rather than doing everything right and encountering unexpected behavior
Honestly, this is a good case for a language called Rescript which is Javascript with OCaml’s type system strapped to it. While I intend to stick with Elm and Mint on the frontend, Rescript is just Typescript but good by breaking backwards compatibility.
Enums in TS suck. Sometimes it only accepts the key, sometimes only the value. Sometimes what is typed as a key is really a value at runtime. Not sure if this counts as lying. Sorry can't remember the specifics just thdt this really bit me
@@igniuss pretty sure if it is a readonly field you can't set it with reflection either. Sure you can set properties with a private setter but if it is actually readonly then I think you'd have to resort to some unsafe code to get at the memory if you wanted to change it.
Assign a method of a class to a property of another class and try to call it. The value for `this` will get changed when it's assigned so it won't work. I solved a bug caused by this like 5 minutes ago.
Another array lie is when you just take an index, like `const item = items[n];` item will become a type of whatever that array is, without considering that it might be undefined if the index is out of bounds. I know this is deliberate, but still risky.
-Primeagens example is fixed with strictFunctionTypes- , yours is fixed with noUncheckedIndexedAccess. *EDIT:* Been wrong about Primagens example. I'm still fairly confident in the solution to your issue 😅
This could be solved by typescript being more strict about what it consideer a duck For me it is clear that a (number | string)[] is structurally different from a string[], and that a readonly T is different from just T, if they could just handle that taking in account the modifiers as part of the structure itself of the type it would solve this problem.
Late to this one but whatever. Here's one of my most hated aspects of TS: implicit interface conformance. type Vec2 = {x: number, y: number} type Vec3 = {x: number, y: number, z: number} function dot2d(a: Vec2, b: Vec2): number { return a.x * b.x + a.y * b.y } const a: Vec3 = {...} const b: Vec3 = {...} dot2d(a, b) // In any normal language, this won't compile Who the hell thought this was a good idea?
@@ThePrimeTimeagen If I wanted duck typing with JavaScript, I'd just use JavaScript. ¯\_(ツ)_/¯ I'd pick TS over JS any day of the week but man this "type safe (haha, you thought)" approach is pretty annoying.
How fucking fast do you type holy shit xD. I have been practicing my typing a lot and was pretty proud of 97 wpm, but I feel like you're at 140 or higher: insane. Anyway I feel like a real idiot because I thought it might be kind of nice when I was learning JavaScript (and TypeScript) that something like this would be a good thing. That in hindsight was pretty stupid of me. Love this video I would like to see more videos of you coding!
You can pass any type variable to a method even if it isn't the specified type to be passed. The other day at work i wrote a method that called (var: number) and did some checks on var, in the else statement, returned var. I did not realize that sometimes var would be passed as a string, ran it, and nothing broke. I was pretty confused when I looked back over.
It's not a bug, but a horrible design decision made on purpose. They justify this by saying that large part of the ecosystem relies on this behavior being possible. Why they haven't just put that in the compiler settings is a mystery.
oh it’s an implicit conversion from the parameter declaration, still horrible, but at least direct consumers can’t modify it - it’s kind of like a PleaseReadOnly type and to do it right in front of the object’s face is just rude, but if you pass it to the mutator, it’s like “hey, I didn’t change it, it was that function, take it up with him”
You can't spread arguments into a constructor function. Ex: contructor(x:number, y: number) Ex: array = [0, 0] Ex: new YourClassName(...array) Problem: I encountered this in typescript, but thinking about it I'm almost positive regular JS throws an error too
@@eqprog spread an array into a constructor? It's effectively the main thing parsers do; reading structure into 2d sequence and lifting it out into a 3d structure. you wouldn't do this particular example.
Having a strongly typed language ensures that you don't have to write runtime checks since the typing forces you to write code that doesn't violate the strict types. This is what TypeScript is supposed to be, but at this point why even have it? If I have to write stupid runtime checks in a strongly typed language, something has gone horribly wrong. Imagine writing runtime checks in C to make sure an int is actually not a char[] or a long long. That would be the stupidest thing ever.
Because typescript is usually dealing with web-based platforms? You can’t always guarantee what kind of data an API may return especially if you don’t own it.
To be fair JS is ugly. TS ultimately compiles to JS unless someone is doing something cool again.. As for the non mutable objects, I personally do something like: const nonMutableObject = { name: "Bob", age: 69 } as const; I know it looks funny with those two consts, but it seems to work for me. The only problem is that you can't do a partially mutable object with this by specifying readonly to a property. I would love the code above to be taken apart if possible. That way we all learn something lol. :D
As const is just syntactic sugar for making a constant object with readonly properties. You can still mutate it technically: For example: function produceReadonlyResult() { return { foo: 69, bar: 420, } as const; } const item = produceReadonlyResult(); function mutateResult(result: Result) { result.foo += 1337; result.bar += 1337; } mutateResult(item);
Well it makes sense as TypeScript isn't smarter than your types, really. TypeScript can only see what input and output you have. What it should have is a way to declare something as mutating, then restrict the types even further (e.g. not allow to fit string[] into (string | number)[] and readonly into non-readonly equivalent types) I'm sure there have been proposals for that.
Well, I don't use TS, but from this it seems like TS is not only not smarter than your types, but is actively dumber than your types. Allowing a string[] to be treated as a (string | number)[] seems exceptionally weird and like a terrible choice, but at least somewhat understandable, but allowing what appear to be two completely unrelated types to be treated the same is utterly baffling. What logic does it even follow to conclude that a ReadonlyResult could possibly be allowed to be passed to a function that takes a Result? Because they're both "Objects?" Because their fields have the same names?
@@sheep4483 Unions are not a TS-only concept. Union is the same as "OR" in bitwise operators-in fact it's also known as a bitwise union. This is why they use the same syntax "|" (pipe) to indicate this. So it makes perfectly sense when you pass a string into a type of (string | number). In this case you're telling it, I can store any value in the array that is either a string or a number. Not as a whole. This would be different from string[] | number[] which would mean, either an array of strings OR an array of numbers. Either way in this case it wouldn't matter, because he is passing a string[] so both those types would fit. The problem here is not the types themselves, it's that TypeScript has no way to guard against mutations in its type-system.
@@dealloc Yes, unions are not a TS-only concept, however the problem is that TS unions do not work the same as unions in the majority of languages that have them. For example, if you had a union StringOrNumber in C, and you have a function that takes that type, you are unable to pass a string or a number into that function, you must give it the union type that it wants, not one or the other. They're entirely distinct, so you must first explicitly convert it. I would say that it makes sense to allow simply interpreting a scalar value as this union type without any explicit conversion in TS, however it's clearly a bad idea for non-scalar values where the value could be mutated. Typescript *could* handle that by giving additional information to the type system, but I would argue it should also first handle that by simply not allowing you to shoot yourself in the foot with the currently existing type system, which I think is what the primary goal of using TS over JS is in the first place. But that still doesn't explain why a ReadonlyResult could ever reasonably be interpreted as Result, could it also interpret a Square as a Triangle?
@@sheep4483 The difference between TypeScript unions and C unions is that C unions are not sum types, whereas TypeScript unions are. Sum types are coproducts of types, meaning they represent either, or both types. C unions doesn't, but also does not prevent you from represent a union U as either a string or a number as they are not type-safe in C. C++ has a std::variant that is a sum type, but are tagged, unlike TypeScript's unions which are untagged (non-discriminated) by default. You can represent tagged unions/descriminated unions in TypeScript by adding additional information, like a label to a type: `{ kind: StringTag, ... } | { kind: NumberTag }`. I agree that it's not ideal that some types shouldn't be compatible-for example readonly vs non-readonly object of the same type. It seems odd to add a `readonly` modifier if it doesn't mean anything in case of objects. However, there are array types that does work as intended; ReadonlyArray does not fit into to mutable Array type, but Array does fit into ReadonlyArray since you cannot mutate ReadonlyArray.
You cane make same thing with "any" type argument: function mutate(a: any){ a.someProperty = "foo"; delete a.secondProperty; } function add(arr: any[]){ arr.push("Something"); }
The lack of a type system would allow any type; a union of everything. Sometimes you want only a union of _some_ things. It still protects you against the rest of types (well, in theory).
@@MrMudbill I get the point of types in general and why they're good, but the thought of letting more than 1 type through seems like an oxymoron. Again, scrub so I don't know what I don't know. Thank you though
Bear in mind that TS transpiles to JS, and JS is weird. For example, maybe you want to explicitly state that the type of something can be null, so you'd use the union type for that. But overall, yeah, I prefer stronger type systems.
@@datguy4104 I like such unions and I don’t think it defeats the purpose of types but it sure as hell makes it more complex so TypeScript better didn’t do this since it has so many stuff it doesn’t do it actually should do. TypeScript is a linter a script tool nothing more.
@@ThePrimeTimeagen-that is wrong.- *EDIT* In fact I'm in the wrong here, as nicely pointed out nicely by lalith below. I'll leave this for posterity. The bivariant parameter and return type issue you were showing is solved with the strictFunctionTypes Flag. This makes parameters contravariant and return covariant, as is correct. Which any competent tooling will enable when generating templates etc btw. It ain't perfect but it's perfectly solvable. Perhaps there's ways around it, but I've never stumbled upon them in my projects. Please consult with people that know a technology before making claims such as that one. The very same behavior is what's holding back Rust.
Use generics instead and specify the return type explicitly. function mutateArray(items: T[], item: T): void { items.push(item); } // This works console.log(mutateArray([1, 2], 3)); console.log(mutateArray(['str1', 'str2'], 'str3')); // This gives an error console.log(mutateArray([1, 2], '3')); console.log(mutateArray([1, '2'], 3)); console.log(mutateArray(['str1', 'str2'], 3)); console.log(mutateArray(['str1', 2], 'str3'));
@@9SMTM6 Oh.. really. Then explain why this is compiling on TS playground and then throwing run time error. You can check on tsconfig that strictFunctionTypes is on (by default). What exactly did this flag solved? I shall be waiting here for my answer. function mutateArray(items: (number | string)[]) { items.push(69); } const items: string[] = ["hello", "world"]; mutateArray(items); console.log(items); function doSomething(items: string[]) { items.forEach(x => console.log(`${x}: ${x.split('')}`)); } doSomething(items);
I was working on a project and I found the following type used everywhere: ``` interface CustomType { [key:string]: any; } ``` This is just obfuscation for using `:any`. Then I also found this: ``` class X implements X { } ```
Wtf! First example defines items as `(number | string)[]` which literarily says the array can contain strings or numbers. The intended behavior would come from `string[] | number[]` 🤷
Typescript is just a very advanced documentation tool for your JavaScript. Nothing more. I sometimes think that a good IDE + JSDoc is a better approach because you don't have false expectations...
There is that but this makes perfect sense when you think about what difference between type and interface/class. The type with read only in it fits in perfectly with Result. It all matches.
Only per default (I think there is a setting for it to not behave like that): const array: string[] = []; function doSometing(str: string) { return str.replace("a", "b"); } console.log(doSometing(array[0])); No type error but will crash because undefined has no property replace.
You can't really with arbitrary type unions (e.g. string | number). Rust doesn't have types like these, because it doesn't need them unlike TypeScript which needs to represent the dynamic types of JavaScript. In Rust you would often use an enum to represent a value that can represent different kinds of values: enum Value { String(String) Number(f64), } In Rust you also have to explicitly mark something as mutable if you want to mutate it. You will also be nagged by the borrow checker to pass mutable references to functions that mutate its value. This is to save you against a lot of bugs and memory safety issues. In Rust references and non-reference is part of the type system in a way. For example &mut T is not the same as &T, nor is mut T or T by itself. They all add constraints to how you can use said value in your code. This is a selling point of the borrow checker in Rust. If Rust had issues with what the video above shows, then it would be a bug in the type system, and you should file an issue on their repository.
Second example is not the best, as Result and ReadonlyResult have same properties and you will not produce an error passing a ReadonlyResult to that function instead of a Result. And THAT is the matter. Try passing a ReadonlyResult instead...
In the second one, since item contains the reference to the object and not the object itself, hence a mutability to the object is allowed but no error is thrown since the reference is still remains the same
I am curious to see you react to "Object-oriented Programming bad" and "Object-oriented Programming is garbage" by Brian Will it's just amazing 👏 he also made a video named "Object-oriented Programming is good*"
I just don't understand why the function is ok accepting a type that just happens to have the same key type pairs. There's nothing defined to bind the two types together so why does TS think they're the same?
TypeScript is structurally-typed instead of nominally-typed, and structural typing is just a slightly fancier version of duck-typing. If you satisfy the basis structure that a function is looking for, that's good enough for TypeScript.
I really don't get ts. If JavaScript type system isn't adequate for your project then maybe js isn't the right tool for your project. One wouldn't use a screwdriver to unscrew a bolt, nor a wrench to unscrew a screw. Both wrench and screwdriver "unscrew" things but doesn't mean they are replaceable. DOM manipulation? Js Backend development? Almost anything else
Yeah, these protections exist for arrays - a readonly array is treated as more specific than a mutable array. That makes sense - you can treat any mutable array as readonly; just don't call the mutable methods. But you can't do the opposite - if you try calling a mutable method on a readonly array, TypeScript will (rightly) yell at you. The problem is that the protections don't exist for objects. Even if you use the Readonly utility type, that doesn't stop the same error Prime showed from happening. All TypeScript objects are structurally-typed, and a mutable/immutable objects are going to be treated as interchangeable, even with the strictest compiler settings. Maybe that can change in the future with some new compiler setting, but we don't have that luxury right now, even with TypeScript 5 on the horizon.
You can run that example on TS playground and see for yourself. Here I typed it out for your lazy ass. Can't share link as automod removes them. I hope copy-paste isn't too much for you. function mutateArray(items: (number | string)[]) { items.push(69); } const items: string[] = ["hello", "world"]; mutateArray(items); console.log(items); function doSomething(items: string[]) { items.forEach(x => console.log(`${x}: ${x.split('')}`)); } doSomething(items);
My main issue with the first example is that within the scope of mutateArray, TS only understands the variable "items" based on type provided in the function signature. If you wanted it to understand it based on the value passed, you should have used generics. I'm not saying TS is perfect. I just think that you should have only talked about the second example since that is a valid one.
This is fixed by using TS enums instead, because those do not get duck-typed. Still, using the enums is tricky and adds complexity because you might end up needing the enum as a field in your object to discriminate. (They are not nice like Rust enums)
The array example more looks like a Python list. You can put whatever items in there you want. The second problem seems inexcusable to me. If 'item' is passed by reference, the function should not be able to mutate it.
I haven't been using typescript as I think it is a steaming pile of crap and a complete waste and don't fully understand how it works. But I assume that internally they are just using JS objects, and checking that the type matches when it is called, not when it is being used? The hilarious thing is you can actually get this type safety in regular JavaScript. You can define a class for any particular type and set it up in a way to get these results. e.g. for the first you can create a class, extending the array class, and have things which add to the array or mutate values in the array check the type before allowing it. Likewise you can define properties on objects which are readonly. The fact that typescript doesn't handle that and doesn't transpile to do that is hilarious and just shows how useless typescript is.
first example can even happen for statically typed language like c# void updateArray(object[] a) { a[0] = new object(); } string[] x = new string[10]; updateArray(x); System.ArrayTypeMismatchException: 'Attempted to access an element as a type incompatible with the array.' To fix these type of things array should be converted to "readonly" when downcasted as argument
HTML, CSS and JS used to suck, back in the day, so you needed junk like jQuery, Sass, Less, Typescript, Bootstrap etc. But now, in 2023, HTML, CSS and JS have advanced so far, there's no need for this extra nonsense. You can actually just code the basics in the native language. The only thing that still sucks is SQL - everyone needs to be using Neo4j databases, it's a game changer.
Wouldn't the first example be mitigated by defining the return type for the function? If you typed it as returning string[], then items.push(69) would throw an error. This could be extended further using genetics, though maybe I'm missing the point. Edit: At first I thought it was returning a new array, and not mutating the args. That's my bad.
He's not returning an array, he's just manipulating it. It is in fact a type error, and there is no holistic solution, differently to what I've claimed elsewhere.
Man typescript at least give you like datatypes like string or number, imagine c where everything is a f*king bytes and the compiler doesn't give a sh*t. In some implementations via pointers you can even MUTATE f*king CONSTANTS