-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
Typescript: setWorldConstructor with anonymous function too restrictive #1968
Comments
If you want to use a function then annotating interface MyWorld extends IWorldOptions {
foo: string
}
function MyWorldCtor(this: MyWorld, props: IWorldOptions) {
Object.assign(this, props)
this.foo = 'asdhasd'
}
setWorldConstructor(MyWorldCtor)
Given('something happens', async function (this: MyWorld) {
// something
}) Really though I'm not sure what problem you're solving by using a function rather than just having a class that extends the builtin |
I did actually realise that by annotating
because at least that means that attach, log and parameters are no longer I agree that class is what's documented, even if the features often use the function style because it suits what they are doing in that context. I think the realisation that I think this is partly about the different between identity ( |
Yeah I think this would benefit from more docs around the |
This has now been released in v8.1.0 on npm |
👓 What did you see?
Whilst its not included in the documentation,
setWorldConstructor
can take either a class name (which is a function), or an anonymous function. This later ability is used numerous times in the features to set custom values on the world instance, conveniently ignoring to assign the props to the World instance, as its not relevant in the context of the test case.For example:
When using a custom class extending
World
, one would pass theprops
argument tosuper
so that the initial props are initialised on the instance.When using an anonymous function,
this
is a plain object, andprops
is an object containing the initial properties (attach, log and parameters).Without any type annotation, Typescript prevents assignment to
this
because it is implicitlyany
(assumingnoImplicitAny
is used intsconfig
.Without a type annotation one gets no protection against typos.
To ensure that Typescript has the correct type for
this
, the convention in Typescript is to annotatethis
as the first parameter, and use theIWorld
type to infer the correct members. One can use interface merging to add any custom properties.The problem here is that one would need to individually apply the props members to
this
or they get lost. But we can't use simple assignment as theIWorld
interface declares them asreadonly
. We can assign custom properties aside from those.Whilst the types allow for assignment to members of
this.parameters
, this is alterating a by reference object which will affect later scenarios.The only solution is to use
Object.assign
to apply the defaultprops
and any custom values, eg:Firstly, this just discards the type safety acquired so far.
Secondly, because
Object.assign
is a shallow copy, it also means that if we wnat to inject something intoparameters
it will affect subsequent scenarios (although World is recreated, we are working on the original (by reference) copy of the world parameters held by Cucumber from the CLI. We can work around this by using spread to create a new parameters object to override the one passed inprops
(necessary asassign
doesn't merge), thus:✅ What did you expect to see?
I think this is somewhat semantic, as if one had sufficiently complex functionality one probably would use a class instead of an anonymous function, but this seems to highlight a possibly unintended side effect of how the object is created when an anonymous function is used, given the restrictive typing of World (which is probably actually correct elsewhere).
For the most part I wanted to document it in case anyone else came across the same problem, rather than to propose/ask for a fix.
A solution that I can see is that if the instance created by the anonymous function is a plain object it could be merged with an instance derived from World, but that might have its own complicated side effects.
📦 Which tool/library version are you using?
🔬 How could we reproduce it?
See above examples
📚 Any additional context?
In my use case I was passing in the name of a class from the world parameters, and wanted to convert it to the constructor within the world parameters. The initial mystery was why the props were not being passed into the World instance (because of not using the World constructor), and then how to make changes to them once the types have been added.
The following is equivilent to how I dealt with it
The text was updated successfully, but these errors were encountered: