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.
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.
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.
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.
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+)
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
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.
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)
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
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.
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.
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.
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 😅
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
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.
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.
@@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.
I jnew "or" types was a mistake in a langauge that tries to be strongly-typed. C# for example does not have such concept of "|" types. very great example that shows this!
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.
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.
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.
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.
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);
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"); }
@@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 { } ```
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.
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.
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...
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!
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”
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
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.
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.
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*"
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.
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.
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 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.
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)
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);
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
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.
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.
Ah, typescript. It's so... almost good
Sounds like a case for Rust
yerp
What isn't!?
Or @effect/data
“Quack” - 🐢
lmaooo
You actually can make an array invariant with 4.7 variance annotations
interface InvariantArray extends Array {}
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
ReScript
Purescript, Rescript
Elm, Rescript, Reason
@@nomoredarts8918 Sadly ReasonML isn't for frontend anymore, I think
3/4 comments mentioned rescript. I was not familiar with it, I'm gonna give it a look. Thanks!
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
what, hows that even work??
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 :)
That is absolutely intended behavior. The never type is the equivalent to the empty set in set theory, meaning anything is assignable to never.
@@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.
Same goes for C++
Pretty sure C and C++ also don't let you do that
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.
interface Foo { bar(n: T): void } would solve your problem.
(I agree with your point though)
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.
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.
let me guess, you work for microsoft now?
@@wrong1029 oof no. I mean, I guess we all do. In the end...
@@michaeljmeyer3 been there. done that. Kudos bro!
My disappointment at the lack of coconut oil in this video is immeasurable and my day is ruined.
Another example which really hurts my brain; you can return a completely different object from a constructor. Really painful
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.
I understand why it happened, it’s why I still resist using the class keyword. Just use functions, use the module to hide private things.
@@MrBaudin Agreed!
We dont even have async constructors :,(
btw, about "E492: Not an editor command: W". You can bind :W command to :w (and also :WQ and :Wq to :wq). It really hapls me a lot...
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+)
we need a compilation in the NotNorm macdonald channel: Constant shitting on typescript, but typescript is a good guy. 🤣🤣
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
yep. just write better types
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.
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)
The read-only thing is so strange..
Since js does have a way to make properties read only that you'd think it would transpile to...
My most hated feature. Tons of type matching P.I.T.A. for literally zero benefits. And you cant turn it off (as const always produces readonly)
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
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.
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.
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.
It seems like "structural typing" is really just Microsoft speak for "duck typing at compile time".
ohh yeah. More footgun unlocked🍻
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.
Yeah, you'd need an option type in javascript to fix that. Probably not happening 😕
-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 😅
@@asdqwe4427 typescript *does* have an option type, it's called `T | undefined`
@@SamualN that doesn’t count lol
There is the "noUncheckedIndexAccess" flag which fixes this behavior
This being the backbone of my everyday stack brings me nightmares
This is why we have languages like Haskell with real type systems.
Yeah most of the good stuff in rusts types is heavily inspired from ML and Haskell
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
One must be clear that typeScript is only statically checking the code at compile time and does not replace proper runtime checks and error handling.
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.
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?
quack quack
@@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.
ReadOnly and privates are just suggestions, including in other languages like C# 😂
C# doesn’t compile without reflection shenanigans if you want to mutate read only fields that aren’t visible
@@Bliss467 but reflection shenanigans are easy as pie 👀
@@igniuss not really
@@igniuss but they're explicit, you meant to do that and it's not some hidden behavior
@@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.
I jnew "or" types was a mistake in a langauge that tries to be strongly-typed. C# for example does not have such concept of "|" types. very great example that shows this!
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.
That second one really looks like a bug, and not a feature, TBH
Union types as array items are a total mess overall imo
you are such a good entertainer and an even better teacher
you should be a QA dev for typescript.. this is a huge bug that made it into production
typescript is unsound and it's known
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.
You know when you run ts code, all type information disappears?
Well technically you don't run TS code, you run JS code.
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.
And typescript is transpiled so this could totally be fixed but they won’t fix it. So tempting to write a better type safe language for JS
It exists: purescript
@@belst_ lol no. anything but ts deviates too far from the base syntax. makes it hard to adopt on existing projects.
@@wadecodez I wish I could say rescript :/
(string[])[0] can be undefined
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
Why would you want to do this?
@@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.
Can you guys tell me, what colorsheme is it?🔥
in the first case your type should have been `string[] | number[]`
3:00
Missing "as const" ?
And how do we help this situation? Like how do you make readonly fields in TS?
Is this related at all to covariance and contravariance?
You're going to piss Teo. I like 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);
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");
}
Okay...but what's the solution ? Use JSDocs ????
no, there is no solution, you have to literally avoid these issues.
Soulsucking way of at least getting it caught during runtime is to freeze the object and it's properties
@@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);
How does you switches between terminal and the nvim, so smoothly ?
ts stands for trashscript
IT'S NOT A DUCK
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 {
}
```
just run
It is not just an obfuscation for any. You cannot say for e.g.
const x: CustomType = 5;
if you use strict type checking, the compiler tells you to explicitly check type which could remove this vagueness.
This is strict mode.
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.
As a C++ (rust --) developer this hurts my brain. why
It's a language level feature, typescript cannot do this
Would be funny if TS was a language. But all good.
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[]` 🤷
The point is that the function made a string[] have a number, but compile time still thinks it's a string[].
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.
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...
How do you get neiovim to be centered like that rather than to the left?
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!
Is this really with all strict compiler options on?
yes
wait so what does the “readonly” keyword do?
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”
what's the ide/code editor here? (newbie question)
Neovim
Time to get the Elm train going.
elm, purescript, fable - this is the way!
nah, it's better to wait for wasm 🦀
i'm sorry what's the name?
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
lovin the vim lol
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.
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.
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*"
Can anyone show an equivalent example of how Rust solves this problem? (I'm not writing in Rust but I'm really curious).
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.
@@dealloc Thank you! I'm going to save your answer for future reference, if you don't mind.
What happened?
Why we're taking too many roundtrips to achieve safety, why not just using Rust
Can't you use "As const" ?
I'm a scrub so this is a genuine question, but doesn't a union type defeat the purpose of a type system? Like why is that there?
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.
@@LeoVital allowing null made it make sense to me
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
so you are saying typescript is not typescript at all
Maybe use brand
This would never happen in rust :p
Where are the solutions for these
Me watching your TS videos makes me love that I don't write TS.
I wonder if TS could work around that with primitive wrappers and Object.seal() and Object.freeze()
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.
weed onwee pweese 🥺 👉👈
avoid mutations like the plague!!
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)
In the first example I literally see nothing wrong?
You declared items is an array of strings, then you mutated it, adding a number, now it's not an array of strings anymore.
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);
@@lalit5408 Wait, but you said "Mutate an array which is number or string array" so technically its fine
at this point just make web servers in java or rust
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
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.
@@9SMTM6 ah yeah I see that now. Fair enough.
Bro wtf. Typescript is just as dynamic as JS. Why tf do I need TS if i can blow types with JS itself.
*Proceeds to remove all type definitions and replace everything with any in his TS repos*
You love your mutations don't you? 😉
Structural typing sucks
it does
On its own it has problems but doesn't suck that much
The horrendous mix of structural typing and nominal typing in TypeScript definitely sucks
I don't think this is a problem specific to structural typing
It does not suck, it ducks.
@@michaelbitzer7295 lmao
May I know the reason for choosing the numbers as 69 and 420?
They're funny.
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.
Js ❤