Skip to content

Type () => T accepts a function that returns an object with properties not inside T. #54661

New issue

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

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

Already on GitHub? # to your account

Closed
LuisFerLCC opened this issue Jun 15, 2023 · 7 comments

Comments

@LuisFerLCC
Copy link

Bug Report

🔎 Search Terms

  • Function return
  • Object
  • Property
  • Properties

🕗 Version & Regression Information

  • This is the behavior in every version I tried, and I reviewed the FAQ for entries about object properties.

⏯ Playground Link

Playground link with relevant code

💻 Code

interface Test {
    a?: string;
    b?: string;
    c?: string;
}

const func: () => Test = () => ({
    a: "",

    // This property should throw an error
    d: ""
});

🙁 Actual behavior

The compiler does not throw any errors, allowing the function to return an object with properties that are not present in interface Test.

🙂 Expected behavior

The compiler should throw an error since property d does not exist on interface Test.

@fatcerberus
Copy link

Types are not exact, excess property checks are not comprehensive, and the object you’ve returned is assignable to Test. See #12936.

@jcalz
Copy link
Contributor

jcalz commented Jun 15, 2023

Ultimately a duplicate of #241 I think

@LuisFerLCC
Copy link
Author

Thanks for clarifying. It's unfortunate that this situation has not been resolved yet.

@LuisFerLCC LuisFerLCC closed this as not planned Won't fix, can't repro, duplicate, stale Jun 15, 2023
@fatcerberus
Copy link

Well, it’s not really a bug. The excess property check is considered a linter feature more than a type system feature so you shouldn’t rely on it to enforce runtime invariants. It’s only designed to catch cases where a fresh object literal is directly assigned to a type, which isn’t what’s happening here. Instead, you have an unannotated arrow function whose return type is first inferred, and then TS asks whether () => TestWithExtraStuff is assignable to () => Test according to usual subtyping rules, which succeeds.

If you really need this check here, you can rewrite the code as

const func = (): Test => ({});

Now you’re directly returning a fresh object literal as a Test so you get EPC.

@LuisFerLCC
Copy link
Author

@fatcerberus Makes sense, though I think a tsconfig option to strictly check function return types might be useful. I'll leave that up to discussion.

@fatcerberus
Copy link

Any stricter behavior here more or less makes this a duplicate of #12936, as otherwise the proposed behavior makes () => Dog not assignable to () => Animal (since all type checks are structural).

@emirotin
Copy link

emirotin commented Jul 18, 2023

I have just encountered the same situation, and I think it's in fact the issue to be considered.

The reason I think so can be illustrated with this simple example:

type Func = () => { a: 1 }

// This is allowed, which is unexpected
const f: Func = () => ({
    a: 1,
    b: 2
})

type Res = { a: 1 };

// And this is not
const res: Res = { a: 1, b: 2 };

My expectations were that object constant and the function return types would undergo the same level of scrutiny, but they do not. I kinda understand what happens with the return type widening, but I still find it an issue.

More specifically, I expect the function type to save me from one of the most common code problems I have: the typos and the similar mistakes.

Now, consider this example:

type Func = () => { 
  id?: string;
  name?: string;
  productType?: string;
}


const f: Func = () => ({
    id: "banana",
    name: "Banana",
    product_type: "tasty"
})

Here, I have very obviously missed the case for the last prop. This will pass the typecheck and introduce the unexpected runtime situation.

Now, I can elaborate on the two cases above and show how this behavior also introduces inconcistency not between objects and functions, but between two different ways to type-annotate the same function:

type Res = { a: 1 };

// This is not allowed
const res: Res = { a: 1, b: 2 };

// This is not allowed, either
function f1(): Res {
  return {
    a: 1,
    b: 2
  }
}

type Func = () => Res

// And suddently, this is OK
const f2: Func = () => ({
    a: 1,
    b: 2
})

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants