It's because the compiler uses the peculiar return type annotation on isWizard() to infer information about the argument and propagate it along one half of the control flow graph based on the result of isWizard().
It sees that the code `person.spells` is guarded by a truthful return from isWizard, and it trusts the annotation that person is Wizard as a result.
The use case is having types depend on control flow so that guarded control flow paths don't need further typecasting. I don't see how this is bonkers. It looks like a great ergonomic feature to me.
As to your code, sure, it's an error. But it's not unusual, conceptually. For example:
if (foo) {
int bar = 22;
}
print(bar); // compilation error??
And this:
if (foo) {
print(bar); // compilation error??
int bar = 22;
}
More idiomatically, you'd expect to read code like this:
if (isWizard(person)) { person.wizardStuff(); }
I think this is not an unusual thing to do in dynamically typed languages, especially when interfacing with third-party code where you can't easily add new polymorphic methods (so you can't rely on virtual dispatch to handle your if-case logic). Making it work more ergonomically in typescript isn't "absolutely bonkers".
Typescripts goal is to allow using existing JavaScript idioms while gaining the advantages of static type checking.
I don't think it's too bonkers for the type of a variable to change within the same scope, as long as there's some clear delineation. In rust you can redeclare a variable with the same name but different type:
let x: i32 = 99;
println!("{}", x);
let x: &str = "shadows";
println!("{}", x);
The type guard returns true if person is a wizard. The if statement handles the case that person is not a wizard. The only case left is that personis a wizard.