Is there a way to specify the type when extending factories?
lo1tuma opened this issue · 7 comments
Given the following types:
interface BaseUser {
id: number;
isAdmin: boolean;
}
interface NormalUser extends BaseUser {
isAdmin: false;
}
interface AdminUser extends BaseUser {
isAdmin: true;
adminPrivileges: string[];
}
type User = NormalUser | AdminUser;
Is there a way to create a factory for the type User
and then a factory for the type AdminUser
which re-uses the User
factory but has the correct type information, so the AdminUser
factory returns objects of the type AdminUser
?
As far as I understand we can only extend the factories like so:
const userFactory = Factory.define<User>(() => {
return {
id: 42,
isAdmin: false
};
});
const adminFactory = userFactory.params({ isAdmin: true, adminPrivileges: [] });
In this case adminFactory
returns the type User
.
My suggestion would to make the following work:
const userFactory = Factory.define<User>(() => {
return {
id: 42,
isAdmin: false
};
});
const adminFactory = userFactory.params<AdminUser>({ isAdmin: true, adminPrivileges: [] });
So that adminFactory
always returns objects of type AdminUser
and only accepts params of DeepPartial<AdminUser>
.
I had similar issues, I currently do type casting to solve this
const userFactory = Factory.define<User>(() => {
id: 42,
isAdmin: false
});
const adminFactory = (userFactory as Factory<AdminUser>).withParams({
adminPrivileges: []
})
This way, you can still get the benefit from the typings.
It would be nice if the library support this though.
Since the approach in #75 doesn’t work I’ve tried out to implement an extend()
method which takes another generator function which must return the diff properties of the extended type compared to the base type and optionally accepts any property from the base type.
Unfortunately I still have a lot of type errors because of incompatibilities in the hook functions. If anyone wants to take a look, here are my changes.
I just wanted to update here that I have been playing around with a complete rewrite of Fishery that I think will address this while also improving the types throughout. Here's a preview of the API I'm working on:
type User = { name: string; admin: boolean };
type AdminUser = User & { admin: true };
type SavedUser = User & { id: number };
const factory = Fishery.define({
build: () =>
({
name: 'Bob',
admin: false,
} as User),
create: user => Promise.resolve({ ...user, id: 1 }),
traits: {
admin: () => ({ admin: true as const }),
},
});
const user = factory.build({ admin: true, name: 'Susan' });
user // typed such that it knows about params, typed as User & { admin: true, name: string };
// create
// if no `create` function defined in the factory, then factory.create() is a type error instead of existing runtime error
factory.create({}); // returns Promise<User & { id: number }> (AKA, a SavedUser)
// extend
const userWithPostsFactory = factory.extend({ posts: [] as Post[] });
const userWithPosts = userWithPostsFactory.build({
posts: [{ id: 1, body: 'post' }],
});
userWithPosts // typed as User & { posts: Post[] }
// traits
const adminFactory = factory.admin(); // builds objects of type User & { admin: true }
const admin: AdminUser = adminFactory.build({});
I already have the types for all of the above built but have yet to implement the code, and there are a lot of missing pieces. I'm planning to dedicate a bit of time in the next month or so to work on this.
Nice, I’m already looking forward to the rewrite 🥳. I also really like that traits can now be specified without subclassing 👍.
const userWithPostsFactory = factory.extend({ posts: [] as Post[] });
Would it be also possible to specify the exact type instead of using a const assertion here? E.g.
const userWithPostsFactory = factory.extend<UserWithPosts>({ posts: [] });
While talking about traits, I’ve recently had very often the need for traits that would produce invalid data, e.g. for testing runtime validation logic. Since the data structures are quite big and I only want to test the absence of e.g. one property I would love to re-use the existing factories. Currently I’m using a combination of fisher
, dot-prop-immutable
and custom factory functions, e.g.
const largeDataStructureFactory = Factory.define<LargeDataStructure>(/* ... */);
function largeDataStructureWithoutProperyFooFactory(): unknown {
const data = largeDataStructureFactory.build();
return dotProp.remove(data, 'foo');
}
Do you think something like that would also fit into fishery?
@stevehanson I've seen you mentioned the rewrite on a few issues—how's progress coming with that?
I'm trying to assess whether to add fishery to our new product—it looks great, but a few of the flexibility-related issues would be very handy.
@samtgarson thanks for checking in. The new version is still a little ways out. I would like to have a beta by the end of the year or early next year, but I can't make any promises as I basically work on this project in bursts when I have the time.
The current version of Fishery is pretty stable and should work great for most cases, but it does lack flexibility for some more complex scenarios, like you mentioned.