Open christoph-fricke opened 2 years ago
Hey, @christoph-fricke. This is a great suggestion, we should export those types to allow developers to annotate their logic safer.
Would you be interested in opening a pull request? I will help you with the code review and getting these changes released.
@kettanaito Sure. I will get to it in the coming days.
Do you think we should only expose selected existing interfaces, which should be the straightforward MVP solution?
It might be useful to add helpers for extracting and mapping types as well. I am not familiar with the existing generic shapes yet, so don't quote me on this 😅, but they might be harder to apply than some helpers that tailor to common use-cases. Right of the bat I am thinking of:
id: PrimaryKey<string>
would be become id: string
.Also I am not sure whether or not the latter is really required. I was thinking of applying it to seed functions but they might work without it. Have to play around with it a bit more:
type UserModel = {} // get this somehow
// This example does not look really useful but gets the idea across.
// I already have seed functions that are more complex and useful.
function seedUser(db: DB, overrides?: Partial<UserModel>) {
return db.user.create(overwrites);
}
I will think about it a bit more and further helpers will crystallize from further usage.
I'd favor a more considered approach. Once we export these types they become our public API. It'd be harder to introduce changes without having our users constantly keep their codebases updated with the correct types (considering users that would use these types).
I feel that some of the internal types we use can be simplified (looking at you, Value
) for the purpose of being used publicly. We may still keep some internal types as the generics structure of the library is extremely complex. We shouldn't expose that complexity to the end-user in any way.
I'd start by writing down a set of usage scenarios when explicit types would be needed. Then focused on exposing those types, refactoring or abstracting where applicable.
@kettanaito I agree. I have tried to use Entity
and Value
by importing them from the internal modules directly without much success. However, over the last weeks I have spend a lot of time writing tests with Playwright against an MSW + MSW Data mock backend and have gained insights into common use cases I encountered all the time. I noticed that all can be properly typed with just one helper type: Value
(2.0).
While I did not manage to use Value
from msw directly - because it does not work with the FactoryAPI
returned by factory
(?!?) - I have hacked together my own Value
type that is derived from the FactoryAPI
:
import { factory } from "@mswjs/data";
import type { ENTITY_TYPE, PRIMARY_KEY } from "@mswjs/data/lib/glossary";
export function createDB() {
return factory({
// ... all the models
});
}
export type DB = ReturnType<typeof createDB>;
/** Helper to get the schema of an model. */
export type Value<Key extends keyof DB> = Omit<
ReturnType<DB[Key]["create"]>,
typeof ENTITY_TYPE | typeof PRIMARY_KEY
>;
Given that Value
is just Entity
without the two internal keys, this helper type can be used in every scenario that I encountered:
function userToDTO(user: Value<"bookmark">): UserDTO
function seedUser(db: DB, opts?: Partial<Value<"user">>): Value<"user"> {
return db.user.create(opts);
}
let user: Value<"user"> | null;
In conclusion, if a type is exposed, I think it should be sufficient to expose a type similar this helper type. Something like Value<DB, Key> = ...
or Schema<DB, Key> = ...
.
Currently I am writing a handler which returns a list of items which can be filtered. In my handler I am constructing a filters
object which I merge into the where
prop of findMany
. For this I find the QuerySelectorWhere
type useful.
import { QuerySelectorWhere } from '@mswjs/data/lib/query/queryTypes';
import type { ENTITY_TYPE, PRIMARY_KEY, FactoryAPI } from "@mswjs/data/lib/glossary";
// Just the types from @christoph-fricke, but I think we need DB as a generic paramter
type Value<DB extends FactoryAPI<Record<string, unknown>>, Key extends keyof DB> = Omit<
ReturnType<DB[Key]['create']>,
typeof ENTITY_TYPE | typeof PRIMARY_KEY
>;
// ...
// Here I want to specify that `filters` is an object with the structure required for my `where` condition
const filters: QuerySelectorWhere<Value<'membership'>> = {};
// And fill it later with values from the `URLSearchParams` of the request.
if (typeof queryParams.role !== 'undefined') {
filters.role = { equals: queryParams.role };
}
if (typeof queryParams.active !== 'undefined') {
filters.active = { equals: queryParams.active };
}
// And finally merge it into the query
const memberships = db.membership.findMany({
where: {
member: { id: { equals: member.id } },
...filters,
},
orderBy: [
{ user: { lastname: 'asc' } },
{ user: { firstname: 'asc' } },
],
});
// ...
In my case here I need those types to create a Domain Transfer Object in msw responses, example using @christoph-fricke solution (renaming to Entity to clarify better)
export type Entity<Key extends keyof DB> = Omit<
ReturnType<DB[Key]["create"]>,
typeof ENTITY_TYPE | typeof PRIMARY_KEY
>;
export const account = {
id: primaryKey(Number),
name: String,
};
export const getAccountDTO = (accounts: Entity<'account'>[]) =>
accounts.map(account => ({
id: account.id,
name: account.name
}));
export const accountsHandler = [
rest.get(`/v1/accounts`, (req, res, ctx) => {
return res(
ctx.json({
items: getAccountDTO(db.account.getAll())
})
);
})
];
I could also use the very same Entity type of account within my api responses and have just a single type for the Account entity across my whole app
The only problem with Chris solution is with relationships. It does not follow the nullable vs non-nullable setting as in some DTOs I had to do something like project.businessUnit?.id
to avoid having type issues since business unit is a one-to-many non-nullable
As far as I am aware this library only exposes types for the exported functions. This makes it hard to type use-cases that go beyond "creating a few models and query them".
My example use-case are mappers to map the models to DTOs, which are returned by an MSW API. These mappers mostly convert relations to their ids and attaches computed information. E.g.
user: oneOf("user")
is mapped touserId: string
. Each mapper should take anEntity
(or maybeValue
🤔) so it kinda look like this:My current approach to type the parameter is the following helper type. However, I actually run into problems when I want to map relations on an entity as well since the mock API requires it, which are of type
Value
. I have no idea how should be able to extract theValue
to use it instead ofEntity
in a similar fashion. Is this a valid use-case or am I totally over-complicating things?