Hi. I’m relatively new to Cursor and to coding with LLMs, so I’m still trying to learn my way around and figure out how best to use them. I’m trying to understand why Cursor produces responses that are different from Claude.ai when they are both supposedly using the same model. Here’s an example of what I mean.
I start with the following function, which I believe was generated with GPT 4o (Claude was down at the time, so cursor fell back to a different model):
export function parseProperty<T extends z.ZodType>(
obj: unknown,
path: string,
schema: T,
): z.infer<T> | null | undefined {
const keys = path.split(".");
let value: unknown = obj;
for (const key of keys) {
if (
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
key in value
) {
value = (value as Record<string, unknown>)[key];
} else {
return undefined;
}
}
if (value === null) {
return schema.safeParse(null).success ? null : undefined;
}
const parsed = schema.safeParse(value);
return parsed.success ? parsed.data : undefined;
}
In Cursor I started a new chat with this file as context, and asked the following: “can you please look over the parseProperty method we have here and confirm that it has the correct logic and is written in the cleanest and most concise way?”
Here is the response from Claude:
I’ve reviewed the parseProperty function, and overall, it’s well-written and follows a logical approach. However, there are a few minor improvements we can make to enhance its clarity and conciseness. Here’s an updated version with explanations:
export function parseProperty<T extends z.ZodType>(
obj: unknown,
path: string,
schema: T,
): z.infer<T> | null | undefined {
const keys = path.split(".");
let value: unknown = obj;
for (const key of keys) {
// Simplified type check
if (value && typeof value === "object" && !Array.isArray(value)) {
value = (value as Record<string, unknown>)[key];
} else {
return undefined;
}
}
// Handle null case first
if (value === null) {
return schema.isNullable() ? null : undefined;
}
// Use schema.parse() instead of safeParse() for more concise code
try {
return schema.parse(value);
} catch {
return undefined;
}
}
Explanations of the changes:
-
Simplified the type check in the loop:
-
Removed the explicit value !== null check, as typeof value === “object” already excludes null.
-
Moved the !Array.isArray(value) check earlier for better readability.
-
Used schema.isNullable() instead of schema.safeParse(null).success:
-
This is a more direct way to check if the schema allows null values.
-
Replaced the safeParse() logic with parse() and a try-catch block:
-
This simplifies the code and reduces the number of lines.
-
It still achieves the same result: returning the parsed value if successful, or undefined if parsing fails.
These changes make the function slightly more concise while maintaining its functionality. The logic remains correct, handling nested object properties, null values, and schema validation as intended.
So I pointed out that it might not be the best idea to remove the null check since typeof null is “object”, and also that replacing 2 lines at the end of the function with 5 lines using a try catch block is perhaps not really more concise, even though the lines are shorter. And then it proceeded to apologize and agree on both counts, and suggested undoing those changes.
Now, for comparison, I decided to go to the Claude website and see what kind of response it produces, expecting it to be fairly similar. I gave it the function for context and used the exact same prompt as I used in Cursor chat. Here was its response:
I’d be happy to review the parseProperty
function for you. Let’s go through the logic and then consider if there are any improvements we can make.
Now, let’s analyze the function.
- The function signature looks correct. It takes an unknown object, a string path, and a Zod schema, and returns the parsed value, null, or undefined.
- The logic for traversing the object using the dot-separated path is correct. It splits the path into keys and iterates through them.
- The type checking for each nested object is thorough, ensuring it’s an object, not null, not an array, and has the key.
- The handling of null values is good, checking if the schema allows null values.
- The final parsing using the Zod schema is correct, using
safeParse
and returning the parsed data if successful, or undefined if not.
The logic appears to be correct. However, we can make a few improvements for cleanliness and conciseness:
- We can use optional chaining and nullish coalescing to simplify the nested object traversal.
- We can use type narrowing to avoid the explicit type assertion.
- We can combine the null check with the schema parsing for a more concise flow.
Here’s an improved version:
export function parseProperty<T extends z.ZodType>(
obj: unknown,
path: string,
schema: T
): z.infer<T> | null | undefined {
const keys = path.split(".");
let value: unknown = obj;
for (const key of keys) {
if (typeof value !== "object" || value === null || Array.isArray(value)) {
return undefined;
}
value = (value as Record<string, unknown>)[key];
}
return schema.safeParse(value).success ? schema.parse(value) : undefined;
}
Key improvements:
- Simplified the object traversal logic by inverting the condition and returning early if the value is not a valid object.
- Removed the explicit null check, as
safeParse
will handle null values according to the schema. - Used
schema.parse(value)
instead ofparsed.data
to ensure the correct type is inferred.
This version is more concise while maintaining the same functionality. It’s also more robust, as it relies on Zod’s parsing to handle null values according to the schema, rather than having a separate check.
Would you like me to explain any part of this improved version in more detail?
These responses are significantly different, with the Claude.ai response being far more interesting and useful. So what is happening here? Why does Cursor produce such a different response to the Claude website, when both are using the same LLM?
On a somewhat related note, I’ve also noticed on several occasions when I ask a question in Cursor and provide the files as context, the response will contain things like this: “this code references function xyz, which probably looks something like this: …” In other words, it’s like even though I’ve provided the files with the relevant code as context and function xyz is right there for it to examine, it acts as if it has no idea about it and just makes an educated guess about what the function might look like. Why would this be?
Apart from those kinds of issues, I’m really enjoying Cursor, and the tab auto-complete is really useful and generally accurate at predicting what I’m trying to do. But with the chat, at times I feel like I’m burning through a lot of my 500 requests just going in circles with Claude because it’s being so difficult to work with, suggesting changes that don’t make sense, offering solutions that we already determined don’t work just a few prompts ago, acting like it can’t see all the context I’ve provided to it, etc.
Sorry for the long post, but curious to hear others’ thoughts.