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? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

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

Already on GitHub? Sign in 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
})

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

No branches or pull requests

4 participants