If you have a non-union type Foo
like this:
interface Foo {
a: string;
}
and if you annotate a variable as that type:
let foo: Foo = { a: "hello" };
then any subsequent assignments or property changes will not affect the apparent type of that variable. For example:
let bar = { a: "hello", b: "goodbye" };
bar.b; // okay
foo = bar; // okay, bar is assignable to Foo
foo.b // <-- error! Property 'b' does not exist on type 'Foo'
Here we have a value, bar
, known to have a b
property. We can assign bar
to foo
because bar
is a valid Foo
(it has an a
property). But once we do that assignment, the compiler has not changed the apparent type of foo
. It's still just Foo
, and therefore we can no longer read a b
property from it.
Note: for union types, it's more complicated because control flow analysis (microsoft/TypeScript#8010) causes some assignments to apparently narrow the variable's type to some subset of the union members. But according to a comment in microsoft/TypeScript#8513, they haven't done this for non-union types.
You annotated d
as type any
, which is a non-union type. After that, any properties you assign will be immediately forgotten.
Furthermore, the any
type is an escape hatch from the type system (see this answer). It is infectious and propagates outward to a lot of other types it touches: any | Foo
is any
; any & Foo
is any
; any["somePropKey"]
is any
. The last one, where you look up a property and get any
is what happened to d.f2
. It's just any
.
If you don't want this to happen, you should stay away from any
and instead use stronger types, preferably more immutable objects, and let the compiler infer instead of manually annotate where possible:
const d2 = {
f2: () => { return "abc" }
}
d2.f2().toUpperCase(); // okay
Playground link to code