Skip to content

Chapter 12 #12

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: initial
Choose a base branch
from
Open

Chapter 12 #12

wants to merge 1 commit into from

Conversation

Andarist
Copy link
Owner

No description provided.


While most of the time we want to have our types remain static, it is possible to create variables that can dynamically change their type like in JavaScript. This can be done with a technique called the "evolving `any`" which takes advantage of how variables are declared and inferred when no type is specified.

To start, use `let` to declare the variable without a type, and TypeScript will infer it as `any`:
Copy link
Owner Author

@Andarist Andarist Jul 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's actually not any, it's magic ;p

it's true that internally TS uses a special kind of any to track this:

var autoType = createIntrinsicType(TypeFlags.Any, "any", ObjectFlags.NonInferrableType, "auto");

and it's true that it can be (confusingly) displayed as any in tooltips but it's not any. If it would be any then the return type of this function should be any, right?

function test() {
  let foo;
  return foo;
}

But the return type here isn't any - it's undefined :) Matching what we can observe here

The fact that any is sometimes displayed is because you check that a write positions - and we can write to it... well, anything. But checking such variables at read positions displays the correct/current type.

I might be really nit-picky here - especially given how it's displayed in tooltips. But how it's displayed in tooltip is a potential subject to change (there is an issue about it!). So in the future, the any-ness of it might not even be user-facing at all. It being any-like in any context is just a... minutiae, an implementation detail really.

// ^?
```

Even without specifying types, TypeScript is incredibly smart about picking up on your actions and the behavior you're pushing to evolving `any` types.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a nice gotcha with this pattern:

const arr = [];

arr.push(1);

const obj = { arr };
//    ^? const obj: { arr: number[]; } 

arr.push("");

Oops.

timeOut: 1000,
};

fetch(options); // No error!
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is not a good example because this is actually an error! (TS playground)

Type '{ timeOut: number; }' has no properties in common with type '{ timeout?: number | undefined; }'.(2559)

The parameter type here is a weak type - so whatever we try to pass in there has to have some overlap with it.


The issue is that the excess properties warning can often make you think TypeScript uses closed objects.

But really, the excess properties warning is more like a courtesy. It's only used in cases where the object can't be modified elsewhere.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • it's a reason why we can have renaming suggestions in those errors like:

Object literal may only specify known properties, but 'timeOut' does not exist in type '{ timeout?: number | undefined; a: number; }'. Did you mean to write 'timeout'?(2561)

Without the excess property checks (and with open objects) this wouldn't be an error so the typo would slip through. But, of course, like pointed out above - it still can slip through quite easily... given how this only is performed on inline object literals.

// ^?
```

By the way, the same behavior happens with `for ... in` loops:
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

funny thing is that TS doesn't complain about this at all (TS playground):

let key: keyof typeof yetiSeason
for (key in yetiSeason) {
  console.log(yetiSeason[key]);
}

it is an intentional type hole 🤷‍♂️ I wouldn't recommend relying on this though


The only things in JavaScript that don't have properties are `null` and `undefined`. Attempting to access a property on either of these will result in a runtime error. So, they don't fit the definition of an object in TypeScript.

When you consider this, the empty object type `{}` is a rather elegant solution to the problem of representing anything that isn't `null` or `undefined`.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just something else that comes to my mind that might explain what it means that TS is a structural type system:

const hasName: { name: string } = () => {} // ok, functions have `.name` property with a string type


In this case, TypeScript shows an error when we try to pass the `Song` class itself to the `playSong` function. This is because `Song` is a class, and not an instance of the class.

So, classes exists in both the type and value worlds, and represents an instance of the class when used as a type.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but u can get the type of a class with typeof Cls if you truly care about the static side of the class

}
```

Both point to the current instance of the class.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It could be helpful to mention what that means more.

With inheritance this will refer to actual current instance of the class that might be derived from this declaring class:

class Song {
  play(): typeof this {
    return this;
  }
}

class RapSong extends Song {}

const oneBeer = new RapSong()

oneBeer.play() // RapSong

};
```

This is because arrow functions can't inherit `this` from the scope where they're called. Instead, they inherit `this` from the scope where they're _defined_. This means they can only access `this` when defined inside classes.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No class in sight and I can utilize this:

function makeObj() {
  return {
    counter: 0,
    createInc() {
      return () => {
        this.counter++;
      };
    },
  };
}


This might seem weird at first - surely these functions are under-specified?

Let's break it down. The callback passed to `handlePlayer` will be called with three arguments. If the callback only accepts one or two arguments, this is fine! No runtime bugs will be caused by the callback ignoring the arguments.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No runtime bugs will be caused by the callback ignoring the arguments.

Until you pass it around 😉

const fn1: (arg: string) => void = (arg: string, arg2?: number) => {};
const fn2: (arg: string, arg2?: boolean) => void = fn1;

fn2("", true); // oops

}
```

This approach avoids the whole issue with the keys, because `Object.values` will return an array of the values of the object. When this option is available, it's a nice way to avoid needing to deal with issue of loosely typed keys.
Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is an interesting observation to be made about Object.values here... if Object.keys returns string[] because the object could have more properties... why the hell this one returns (string | number)[]?

const test2 = Object.values({ a: 1, b: '' })

The same could be said about Object.values... the object could contain just any properties and just any values. So if TS is concerned about it... the return type here should really be unknown[].

@Andarist Andarist marked this pull request as ready for review July 21, 2024 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant