mswjs / data

Data modeling and relation library for testing JavaScript applications.

Home Page:https://npm.im/@mswjs/data

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Find APIs should possibly return undefined

camchenry opened this issue · comments

Hi, I am having multiple issues, the biggest of which currently is that the findMany and findFirst APIs do not allow for the possibility of returning undefined, even though they may do so. This causes TypeScript to not be able to type check this properly and results in run-time type errors.

CodeSandbox example:
https://codesandbox.io/s/mswjs-undefined-type-error-omifi?file=/src/App.tsx

import { factory, primaryKey } from "@mswjs/data";
import {
  EntityInstance,
  PrimaryKeyDeclaration
} from "@mswjs/data/lib/glossary";
import * as faker from "faker";

const db = factory({
  user: {
    Id: primaryKey(faker.datatype.uuid),
    firstName: () => "random data here"
  }
});

// Seed the database
db.user.create({
  Id: "ab4f631c-cca4-498f-a5aa-4828352a7c69",
  firstName: "Test"
});

// Some time later, query the database:
// Get a user (e.g., GET /user/:userId call)
const user = db.user.findFirst({
  where: {
    Id: {
      equals: "ab4f631c-cca4-498f-a5aa-4828352a7dawdawdc69"
    }
  }
});

// OK
console.log("This will NOT cause a run-time type error: ", user?.firstName);

// ERROR
console.log("This will cause a run-time type error: ", user.firstName);

I think these APIs should have the type like T | undefined (where T is the entity) rather than just returning T unconditionally.

Hey, @camchenry. Thank you for yet another suggestion!

Would T | null work for your use case? It'd be great to align the returned value, and we do use null for update/delete operations already:

data/src/glossary.ts

Lines 136 to 140 in f8da857

update(
query: QuerySelector<Value<Dictionary[ModelName], Dictionary>> & {
data: Partial<UpdateManyValue<Dictionary[ModelName], Dictionary>>
},
): EntityInstance<Dictionary, ModelName> | null

I have noticed this some day ago and put it in the pr #67. Today i'll rebase the branch

Yeah, T | null would work for me, and I'd imagine others as well. For pretty much all intents and purposes, I'd treat null and undefined interchangeably. For me, the main thing is being able to use the ??, and ?. operators to handle nullish values seamlessly, as well as doing a check like:

const entity = db.entity.findFirst(...);
if (!entity) {
  throw new Error(); // or return 404
}
// entity is now guaranteed to not be null/undefined

Thinking about it a little more though, this should probably only apply to the findFirst function, because it returns the entity directly rather than an array of entities. Because findFirst is analogous to Array.find and findMany is analogous to Array.filter?

Agree on that: let's retain findMany returning an empty array in the case when no entities matched the query. I find it a more pleasant data type to work with (both results.length and results.map are intuitive on empty results).

I believe with @67 merged findFirst and findMany now return the expected types:

data/src/glossary.ts

Lines 129 to 141 in 5cc1d73

/**
* Find a first entity matching the query.
*/
findFirst(
query: QuerySelector<Value<Dictionary[ModelName], Dictionary>>,
): Entity<Dictionary, ModelName> | null
/**
* Find multiple entities.
*/
findMany(
query: WeakQuerySelector<Value<Dictionary[ModelName], Dictionary>> &
BulkQueryOptions,
): Entity<Dictionary, ModelName>[]