#Confusions with TS Basics

73 messages · Page 1 of 1 (latest)

jovial olive
#

I'm just starting with TS and after reading the docs I have a few aspects I'm very confused about:
[1] Isn't var useless when const and let exist?
[2] Isn't == and != bad practise because it allows implicit type conversions, so should I always use === and !== instead?
[3] From what I read, someVar! / someVar as X and <X>someVar don't actually do any assertions or conversions, but instead tell the compiler to shut up about potential errors
[4] Static properties of classes aren't recognized as undefined if not initialized? For instance below, x has the type number instead of number | undefined

class Example { public static x: number;}

[5] There is no proper overloading? Instead one has to define a function which combines all functions to overload, where the parameters have to take on all possibilities of the overloaded function parameters and this ambiguity has to be handled in this combined function body...
[6] My biggest concern is with using object (and in turn type, interface), because it feels so loose, for instance:

interface Point { x: number, y: number }
interface WeirdPoint { x: number, y: number }
function Example(): Point | WeirdPoint {...}
const test = Example();

is there no way for me to know which type test has? Its type is object, but how shall I distinguish between it being a Point or a WeirdPoint?
Objects seem to obey the mantra "if it quacks like a duck then it's a duck" even when this leads to really confusing code where one must be cautious not to mismatch interfaces/types accidentally because they have similar arguments or when having a function that takes an object with Properties "a" and "b", maybe one accidentally now allowed a type one didn't know about elsewhere which has these two properties but wasn't intended for this purpose.

errant wind
#
  1. Yes, don't use var. Prefer to use const everywhere, and use let only if necessary

  2. Yes don't use == and !=, except maybe == null or != null so that it would check for both null and undefined in one go

jovial olive
torn light
#

Usually you just pick one of null and undefined rather than both so you don't need to do that.

jovial olive
torn light
#

undefined is imo the better choice, the entire JS language pretty much uses undefined.

#

3 and 5 are both unsafe language features, so you just don't use them.

jovial olive
torn light
#

6 is just the difference between structural type system vs nominal type system. There are pros and cons to both, some patterns are difficult to express in nominal type system (even C# uses duck typing for various features like .GetAwaiter/object initializer/collection initializer/IDisposable for structs/etc)

errant wind
#
  1. These are doing type assertions. It just doesn't have any effect at runtime. It doesn't silence all errors at said location

For example if a variable abc had type number | undefined and you use abc!, it still doesn't let you access methods of string objects like toUpperCase. It just tells TS compiler to assume that abc is not undefined, but always number

jovial olive
torn light
# jovial olive oh overloading is unsafe?

Yes, TS exists also to allow an upgrade path for existing JS projects, and some JS code are too dynamic, and overload is one of the TS features to allow you to describe those code. In a fresh TS project, you basically just don't use them.

errant wind
#

Well there are cases assertion would be necessary
Usually for library level stuff
Even then prefer using as SomeType instead of <SomeType>

jovial olive
jovial olive
torn light
#

TS structural typing on top of having JS language level features like spread operator, actually works a lot better when you go the functional style over OOP. But there's nothing stopping you from writing TS just like C#.

errant wind
#

Just write different functions, no overloads

jovial olive
torn light
#

But yeah, both non null assertion and type assertion, are unsafe. Similar to null forgiving in C# is also unsafe and shouldn't be used.

jovial olive
torn light
#

But why do you need to know which one it is?

jovial olive
#

as I may use it differently afterwards

#

take a different example, Vector and NormalizedVector

torn light
#

Then you need some way to distinguish them. As it stands, Point and WeirdPoint are completely equivalent in the code you posted.

jovial olive
#

if test were a NormalizedVector, I would know its length is 1

jovial olive
#

so there's no way to distinguish them with the current interface defintions?

torn light
#

No, it's just like C# (int X, int Y) is the same as (int Width, int Height).

#

It completely depends on why you want to differentiate them.

jovial olive
#

I may call two different functions depending on their type afterwards

#

so would I need to add a property to Vector and NormalizedVector to distinguish them, e.g. a property "type"?

errant wind
#

Yes but even then, there is nothing stopping me from doing this
vec.x = vec.x + 5
Which would change the length

Unless you freeze the object

jovial olive
torn light
#

How would you do it in C#?

  • In C#, you might do a pattern matching:
if (test is Point)
{
    // do some point specific code
}
else
{
    // do some weird point specific code
}

That's the same in TS, the only difference is that C# has type information attached to every single object, but in TS you need to attach those information yourself:

type Point = { type: 'normal', x: number, y: number }
type WeirdPoint = { type: 'weird', x: number, y: number }

if (test.type === 'normal') {
    // do some point specific code
} else {
    // do some weird point specific code
}
  • In C#, instead of pattern matching, you might put those specific code in your instance methods:
class Point
{
    void DoSomething() {}
}

class WeirdPoint
{
    void DoSomething() {}
}

// you no longer need to check for type
test.DoSomething();

And in TS you would write the exact same code, whether using classes or POJOs.

jovial olive
#

just odd that it has to be done explicitly

#

instead of an object storing its type automatically if it is created for that sense

torn light
#

C# has runtime type information available for every single object. It's convenient but also overhead.

#

If you want type information attached to a JS object, classes do the same thing.

#

C#:

class Point {}
class WeirdPoint {}

if (test is Point)
{
    // ...
}

JS/TS:

class Point {}
class WeirdPoint {}

if (test instanceof Point) {
    // ...
}
jovial olive
#

yop! :]

torn light
#

If you go the functional style in TS, you will have to attach those information yourself.

jovial olive
#

isn't it also dangerous to e.g. have a parameter for a function which must simply obey having two properties called "x" and "y" as numbers? I've seen that in the docs too

#

since it would allow the coder to call the function with any object which has these two properties

#

even it wasn't intended for that object

torn light
#

Even C# uses duck typing for various features like .GetAwaiter/object initializer/collection initializer/IDisposable for structs/etc

jovial olive
#

hm, lastly, do you know if [4] is done right this way?

#
class Example { public static x: number;}
#

I was confused why x would be seen as of type number instead of number | undefined

#

because it receives the value undefined

torn light
#

I generally just write code that the language is best at. For C# I write in OOP style, for TS I write in functional style.

jovial olive
#

but noticed I couldn't treat it as number as it was undefined

#

nywys thx a lot @torn light & @errant wind :)

torn light
jovial olive
#

!resolve

torn light
#

In your example, Point and WeirdPoint would be two equivalent interfaces in C#, and since every object implicitly implement every applicable interface, then all Points also implement WeirdPoint, and vice versa, so there's no way for you to tell them apart.

jovial olive
torn light
#

In order to strongly type an object that is not created from a class, they have to effectively implement every applicable interface, otherwise const point: Point = { x: 42, y: 69 } isn't assignable.

jovial olive
torn light
#

There really isn't that big of a difference really, even in C# there are language features that are functional than OOP and you use them a lot. Eg pattern matching test is Point is functional, you wouldn't write that in strictly OOP. LINQ is very much functional inspired.

jovial olive
jovial olive
torn light
#

The difference is really just "OOP encapsulates state and behavior in every object itself" vs "functional separates state and behavior, states are pure and behaviors operate on those pure states"

#

A classic example written in OOP style:

abstract class Shape {
    abstract calculateArea(): number
}

class Circle extends Shape {
    radius: number

    constructor(radius: number) {
        super()
        this.radius = radius
    }

    calculateArea() {
        return Math.PI * this.radius * this.radius
    }
}

class Rectangle extends Shape {
    width: number
    height: number

    constructor(width: number, height: number) {
        super()
        this.width = width
        this.height = height
    }

    calculateArea() {
        return this.width * this.height
    }
}

new Circle(5).calculateArea()
new Rectangle(3, 4).calculateArea()

The same code in functional style:

type Circle = { type: 'circle', radius: number }

type Rectangle = { type: 'rectangle', width: number, height: number }

type Shape = Circle | Rectangle

function calculateArea(shape: Shape) {
    switch (shape.type) {
        case 'circle':
            return Math.PI * shape.radius * shape.radius
        case 'rectangle':
            return shape.width * shape.height
    }
}

calculateArea({ type: 'circle', radius: 5 })
calculateArea({ type: 'rectangle', width: 3, height: 4 })
spare sparrow
half zenith
#

1 and 2 exist because originally js wanted to be easy to get into, so those features are kinda sloppy
let/const and ===/!== were added later and are pretty much replacements.