jamesmcnamara / shades

A lodash-inspired lens-like library for Javascript
MIT License
413 stars 14 forks source link

struggling with nullable values #42

Closed m-rutter closed 2 years ago

m-rutter commented 4 years ago

Hi, I really like the look of the library and I'm trying to compare it against things like monocle-ts.

I'm struggling a bit figuring out how to deal with objects with nullable values. I'm dealing with things generated by graphql codegen, which tends to generate types full of nullable values.

Consider this interface for example which is very typical of what graphql codgen generates:

type Maybe<T> = null | T;

interface Thing {
  __typename?: "Thing";
  foo?: Maybe<{
    bar: {
      __typename?: "Bar";
      otherValuesThatICannotProvideDefaultsFor: {
        value: string;
        __typename?: "Boo";
      };
      bazs: ReadonlyArray<{
        __typename?: "Baz";
        value: number;
      }>;
    };
    __typename?: "Bar";
  }>;
}

declare const thing: Thing;

get("foo", valueOr({ bar: { bazs: [] } }), "bar", "bazs")(thing); // tsc type error, but at runtime returns empty array

I think in the case of things like monocle-ts I would use stuff like Optional to getOptions of the result of my equilivent get and mod functions. What would I do here? It might be nice to have some documentation examples of how to deal with nullable values. I've tried using valueOr to provide default values, but I get various type errors:

Argument of type 'Thing' is not assignable to parameter of type 'HasKey<"foo", { bar: { bazs: never[]; }; } | null | undefined>'.
  Type 'Thing' is not assignable to type '{ foo?: { bar: { bazs: never[]; }; } | null | undefined; }'.
    Types of property 'foo' are incompatible.
      Type '{ bar: { __typename?: "Bar" | undefined; otherValuesThatICannotProvideDefaultsFor: { value: string; __typename?: "Boo" | undefined; }; bazs: readonly { __typename?: "Baz" | undefined; value: number; }[]; }; __typename?: "Bar" | undefined; } | null | undefined' is not assignable to type '{ bar: { bazs: never[]; }; } | null | undefined'.
        Type '{ bar: { __typename?: "Bar" | undefined; otherValuesThatICannotProvideDefaultsFor: { value: string; __typename?: "Boo" | undefined; }; bazs: readonly { __typename?: "Baz" | undefined; value: number; }[]; }; __typename?: "Bar" | undefined; }' is not assignable to type '{ bar: { bazs: never[]; }; }'.
          The types of 'bar.bazs' are incompatible between these types.
            The type 'readonly { __typename?: "Baz" | undefined; value: number; }[]' is 'readonly' and cannot be assigned to the mutable type 'never[]'.ts(2345)
m-rutter commented 4 years ago

my tsconfig:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react",
    "downlevelIteration": true
  },
  "include": ["src"]
}
jamesmcnamara commented 2 years ago

Sorry for the multi-year delay; I can't imagine it's still relevant but if it is, or it is to someone else, the issue is simply that you were trying to fill in the ReadonlyArray<{ __typename?: "Baz"; value: number; }>; with a normal array and the error is saying that you can't assign a mutable list to a read only list. Specifying that your list is not mutable would resolve this:

type Maybe<T> = null | T;

interface BazT {
  __typename?: "Baz";
  value: number;
}
interface Thing {
  __typename?: "Thing";
  foo?: Maybe<{
    bar: {
      __typename?: "Bar";
      otherValuesThatICannotProvideDefaultsFor: {
        value: string;
        __typename?: "Boo";
      };
      bazs: ReadonlyArray<BazT>;
    };
    __typename?: "Bar";
  }>;
}

declare const thing: Thing;

get("foo", valueOr({ bar: { bazs: [] as ReadonlyArray<BazT> } }), "bar", "bazs")(thing);