1

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?

6
  • 1
    When retrieving JSON from the nextwork, you don't have a concrete type. If you assert that your JSON is of the incorrect type, then all bets are off. To avoid this, you could create type guards to ensure that your data is the correct shape, or, in the extreme, use a validator ( eg github.com/ajv-validator/ajv ) to be 100% robust. Commented Aug 19, 2021 at 20:24
  • 2
    A function whose signature is <T extends Foo>()=>T like your getIt() is always going to be problematic. How could you possibly safely implement something that claims to return any subtype of Foo the caller wants when the caller doesn't pass any arguments in at runtime? You're almost required to implement with a type assertion or the any type to get it to compile, and then you're being unsafe intentionally. Commented Aug 19, 2021 at 20:35
  • 2
    you say Bar is different from Foo, and does not implement/extend Foo but this is not true. Bar does indeed implement Foo; it's just not declared to do so. TypeScript's type system is structural, not nominal. Commented Aug 19, 2021 at 20:37
  • and The compiler emits an error if I try to do this because the type is different from the Foo interface. is also not true exactly. { a: 'a', b: 0, c: true } is 100% compatible with Foo, but the compiler has a linter-like rule called excess property checking that warns you if you assign an object literal with extra properties to a variable or property that will ignore them. Commented Aug 19, 2021 at 20:41
  • I... think there's a lot going on in the example code and code comments, so I'm not sure how to answer this without a lot of explanations about how TS works. Maybe you could pare down the post to be asking one particular question and we can answer it? Commented Aug 19, 2021 at 20:42

1 Answer 1

1
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 */;
}
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.