Reputation: 345
I am seeing a case where the TypeScript compiler lets me assign a value to a variable that is not the correct type. This results in runtime errors that should get caught at compile time:
// An interface to represent JSON data stored somewhere.
interface Foo {
a: string,
b: number,
}
interface Boo extends Foo {
c: boolean,
}
// A class that has some of the same fields as Foo, but has more stuff going on.
class Bar {
get frank(): string { return this.a + this.b; }
get a(): string { return 'a'; }
get b(): number { return 5; }
greet() { console.log(this.frank, this.a, this.b)};
}
// Some function that retrieves JSON data from where it is stored. Might be Foo or a different data type that extends foo.
function getIt<T extends Foo>() : T {
return { a: 'hi', b: 38 } as T; // Hard coded for the example.
}
// The compiler emits an error if I try to do this because the type is different from the Foo interface.
const notReallyFoo: Foo = { a: 'a', b: 0, c: true };
// The compiler does let me do this even though Bar is different from Foo, and does not implement/extend Foo.
const notReallyBar: Bar = getIt();
notReallyBar.greet(); // Runtime error because the object does not have the greet method.
Is there some change I need to make so errors like this will be caught at compile-time? Is there a better way to approach this?
Upvotes: 1
Views: 188
Reputation: 95754
function getIt<T extends Foo>() : T {
return { a: 'hi', b: 38 } as T; // Hard coded for the example.
}
It looks like you're intending getIt
to return "an unknown extension of type Foo, which might be Boo, Foo, or Bar". However, the syntax you've described here is "the compiler will choose an appropriate type T that extends Foo and fill in the signature accordingly". Naturally, that's very difficult for the compiler to enforce—how can you really return any subtype of Foo when you don't even control what that type is?—which is why your as T
is doing some type-unsafe heavy lifting in your example. This is what allows your line below to work:
// the compiler sets T = Bar, which is "fine" since
// structurally Bar extends Foo
const notReallyBar: Bar = getIt();
To express that your JSON will decode to at least the properties of Foo, you should simply return type Foo. In your non-hard-coded example, as jcalz notes in the question comments, you won't need to worry about the excess property checking; you could always add as Foo
temporarily where you have as T
, which is significantly safer than the as T
and generic.
function getIt() : Foo {
return /* your value here */;
}
Upvotes: 1