TypeScript
TypeScript copied to clipboard
Rename / Find All References should find properties in indirectly contextually-typed object literals
Bug Report
🔎 Search Terms
- object
- generic function
- argument
- parameter
- contextual type
- rename
- go to definition
- find references
🕗 Version & Regression Information
- This is the behavior in every version I tried, and I reviewed the FAQ for entries about contextual types
⏯ Playground Link
N/A
💻 Code
It is desirable for objects to have type annotations so that language server features can work correctly on object properties i.e. "rename" / "go to definition" / "find references".
All of the following examples have type annotations however these language server features do not work.
This seems to be because the contextual type does not flow through to the argument provided to the generic function.
Would it be possible to change this behaviour so that the contextual type does flow through? Specifically when the argument is an object literal expression.
declare const identity: <T>(x: T) => T;
type User = { name: string };
const value: User = identity({ name: "bob" });
As you can see in this demo, if we rename a property, the rename is not propagated. This is because TypeScript doesn't seem to understand the relationship between the object literal expression and the type, despite the fact that we've provided a type annotation very close to the object definition.
https://user-images.githubusercontent.com/921609/191320648-5eefb89b-4d9b-47cf-86f0-629adce5a4dc.mov
A real world example of this that comes up all the time is when we use functions such as O.some
from fp-ts
.
import * as O from "fp-ts/Option";
type User = { name: string };
const value: O.Option<User> = O.some({ name: "bob" });
Here's another real world example that comes up all the time when using a pipe
function such as the one defined in fp-ts
:
declare function pipe<A, B>(a: A, ab: (a: A) => B): B;
declare const logName: (user: User) => void;
type User = { name: string };
pipe({ name: "foo" }, logName);
This seems to be because the contextual type does not flow through to the argument provided to the generic function.
The contextual type does flow through; proof:
declare const identity: <T>(x: T) => T;
type User = { name: (x: string) => void };
// Incorrect argument correctly flagged
const value: User = identity({ name: a => a.toUpperCase(3) });
Is there a type system observation of the problem you're describing, or is it just that find all / rename isn't working as expected?
My bad—you're right!
is it just that find all / rename isn't working as expected?
It's just that.
Out of interest, if the contextual type does flow through as you pointed out, why does find all / rename not work? I guess that the value { name: "bob" }
is not given the type User
—TypeScript just checks that it's assignable to the contextual type User
?
Is there a type system observation of the problem you're describing
Actually, I think this might be one?
declare const identity: <T>(x: T) => T;
type User = { name: string; age?: number };
const value: User = identity({
name: "bob",
// Typo. This should be flagged as an error, but it's not.
ageeeeee: 42,
});
That's a different issue (I think...), identity
first has its T
inferred from its argument, and then its return type ends up being assignable to { name: string, age?: number }
so there's no excess property error because by that point the object literal is no longer considered "fresh" (i.e. it's checking the return value of a function, not a direct object literal). The contextual type User
presumably has a lower priority as an inference candidate than the argument you actually supplied.
Basically, excess property checks aren't part of the normal type checking flow and generally rely on explicit type annotations; cases where you're letting types be inferred will usually bypass them.
Contextual typing doesn't play into excess property checking (perhaps surprisingly)
declare const identity: <T>(x: T) => T;
// Rename here renames alice but not bob
type User = { name: string };
const a: User = { name: "alice" };
const b: User = identity({ name: "bob" });
@RyanCavanaugh Thanks for clarifying and updating this issue.
Do you have any thoughts with regards to the behaviour I described for excess properties? Is this something that could be changed/fixed as well? I suspect there's already another issue for this.
Separately, but related, I'm trying to write a lint rule to require type annotations for objects. The rationale and my WIP can be found here: https://github.com/typescript-eslint/typescript-eslint/pull/5666. I have managed to make good progress just by checking that the ObjectLiteralExpression
has a contextual type and reporting a lint error if it doesn't, but this doesn't work in the scenario above where we pass an object into a generic function: it seems the object does have a contextual type but that contextual type seems to be inferred from the object itself (rather than the context).
This is what I have so far. Would you say this is correct? Or is there another way to capture this?
const checkObjectHasNoType = (n: ts.ObjectLiteralExpression): boolean => {
const contextualType = checker.getContextualType(n);
return (
contextualType === undefined ||
/**
* Contextual type is inferred from this object node.
*
* Note: if two nodes are the same node they will be equal by reference.
*
* Examples:
* - object passed as a function argument where the parameter type is generic, e.g.
* `declare const f: <T>(x: T) => T; f({ prop: 1 });``
* - object as a function return value where the return type is generic, e.g.
* `[].map(() => ({ prop: 1 }))`
*/
n === contextualType.getSymbol()?.valueDeclaration
);
};
Depends what precisely you want to happen, but that seems like the right approach.