Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Is there a way to specify the type when extending factories? #71

Open
lo1tuma opened this issue Aug 4, 2021 · 7 comments
Open

Is there a way to specify the type when extending factories? #71

lo1tuma opened this issue Aug 4, 2021 · 7 comments

Comments

@lo1tuma
Copy link

lo1tuma commented Aug 4, 2021

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>.

@jeffrey-effendy
Copy link

jeffrey-effendy commented Aug 5, 2021

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.

@lo1tuma
Copy link
Author

lo1tuma commented Sep 20, 2021

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.

@stevehanson
Copy link
Contributor

stevehanson commented Dec 21, 2021

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.

@lo1tuma
Copy link
Author

lo1tuma commented Dec 22, 2021

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?

@samtgarson
Copy link

@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.

@stevehanson
Copy link
Contributor

@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.

@maslade
Copy link

maslade commented Oct 10, 2024

This isn't as nice as what @lo1tuma recommended at the top, but I'm using this pattern:

interface BaseUser {
  id: number;
  isAdmin: boolean;
}

interface NormalUser extends BaseUser {
  isAdmin: false;
}

interface AdminUser extends BaseUser {
  isAdmin: true;
  adminPrivileges: string[];
}

type User = NormalUser | AdminUser;

const baseUserFactory = Factory.define<BaseUser>( () => ( {
  id: 42,
  isAdmin: false,
} ) );

interface TransientParams {
  baseUser: BaseUser;
}

const adminUserFactory = Factory.define<AdminUser, TransientParams>( ( { transientParams: { baseUser } } ) => ( {
  ...( baseUser || baseUserFactory.build( { isAdmin: true } ) ),
  isAdmin: true,
  adminPrivileges: [],
} ) );

const baseUser = baseUserFactory.build();
const adminUser = adminUserFactory.build();
const adminUserWithBaseUser = adminUserFactory.build( {}, { transient: { baseUser } } );

I'm not extending using Fishery semantics, but via simple object spreading. As as result, my adminUserFactory doesn't inherit any of the behaviors from baseUserFactory, which is a drawback. But with a little extra work, I can take advantage of the baseUserFactory by creating my own base user and passing it in as a transient param to the adminUserFactory.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants