fabian-hiller / valibot

The modular and type safe schema library for validating structural data 🤖
https://valibot.dev
MIT License
6.32k stars 204 forks source link

Question about "pick" Function - Incorrect TypeScript Types for Non-Selected Keys #891

Closed JacKyDev closed 4 weeks ago

JacKyDev commented 1 month ago

Hi everyone,

I have a question about the "pick" function. The documentation describes it as:

"pick creates a modified copy of the given object schema that contains only the selected keys. It is similar to TypeScript's Pick utility type."

I have the following scenario: I'm working with a base schema and want to extract specific attributes using pick by specifying keys.

This process works both in the browser and in basic validation. However, I noticed an issue with the Developer Experience (DX). The output in TypeScript still shows all attributes from the original schema, which leads to a problem: If there are keys in the original schema that are not optional, TypeScript indicates that everything is fine, but the browser throws an "undefined" error.

Here's an example to illustrate the issue:

import {object, string, number, ObjectKeys, optional, pick, safeParse} from "valibot";

const Product = object({
    code: string(),
    name: string(),
    price: object({
        currencyIso: string(),
        value: number()
    }),
    additional: optional(object({
        description: string()
    }))
});

const featureWantedProductFields: ObjectKeys<typeof Product> = [
    "code",
    "name"
];

const result = safeParse(pick(Product, featureWantedProductFields), {
    code: "123",
    name: "Product Name",
});

if (result.success) {
    // failed with error in browser
    // in typescript a valid case without problems
    console.log(result.output.price.value);
}

In TypeScript, it behaves in a way that result.output still has access to price and its attributes, even though pick should only include code and name. This results in no errors in TypeScript, while the browser throws a "price is undefined" error.

My expectation is that pick would behave similarly to TypeScript's Pick utility, making only the selected attributes available in the type. Alternatively, it could mark all indirectly excluded attributes as optional in TypeScript to minimize potential errors.

Am I on the right track here? Is there a way to achieve the expected behavior correctly, or am I missing something?

Additionally, I want to mention that I find the library excellent - I haven't seen a package with such potential in a long time! 😊

I'm not sure if this behavior qualifies as a bug or if I'm just misusing the function. I would appreciate any help or suggestions on how to resolve this issue.

Thanks in advance!

fabian-hiller commented 4 weeks ago

Please change:

const featureWantedProductFields: ObjectKeys<typeof Product> = [
    "code",
    "name"
];

To:

const featureWantedProductFields = ['code', 'name'] as const;

Otherwise, TypeScript will never know what exact keys you passed to pick.

JacKyDev commented 4 weeks ago

Oh, that's a shame. I was hoping that through the type definition, the developer would be guided on which fields they can use without having to look into the schema.

But okay, I tested it, and it works in the output case, which I definitely prefer.

I thought it would help the developer to know which attributes are available without always having to search through the schemas.

But yes, it works. Thank you very much!

fabian-hiller commented 4 weeks ago

Maybe this helps:

const featureWantedProductFields = [
  'code',
  'name',
] as const satisfies ObjectKeys<typeof Product>;
fabian-hiller commented 4 weeks ago

Or if you write a function you could write:

function foo<const T extends ObjectKeys<typeof Product>>(keys: T, …) {…}
JacKyDev commented 4 weeks ago

The const satisfies ObjectKeys<typeof Product>; sounds very interesting. I will also create a function to always automatically add primary keys, but this could help ensure that the developer doesn’t have to dive too deeply into the details to implement a feature. Thanks again, I'll take a look at how I can integrate this into my structure :)