-
-
Notifications
You must be signed in to change notification settings - Fork 8
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
feat: derive constructor options from chainable Base.defaults()
calls
#57
Conversation
bf15c10
to
964c604
Compare
I asked a question about this on StackOverflow: |
progress (I think): I managed to create a function which accepts both defaults and options, and all the type behavior behaves as expected: type Optional<T extends object, K extends string | number | symbol> = Omit<T, K> & Partial<Pick<T, keyof T & K>>;
type Options = {
requiredOption: string;
optionalOption?: string;
};
function testWithDefaults<TDefaults extends Options>(defaults: TDefaults);
function testWithDefaults<
TDefaults extends Partial<Options>,
TOptions extends Optional<Options, keyof TDefaults> & Record<string, unknown>
>(defaults: TDefaults, options: TOptions);
function testWithDefaults(
defaults: Record<string, unknown>,
options?: Record<string, unknown>
) {
return {
...defaults,
...options,
};
}
const test = testWithDefaults({ requiredOption: ""}, {}) |
I've spent way too much time on this already. The answers to my the StackOverflow question do not work, because they both only implement a single I've reached out to @JoshuaKGoldberg for more help and their assessment is that it's not possible with today's TypeScript features. Josh opened two issues on the TS repository
two other related issues are
I'd like to try one more approach: limit the amount of chaining to 3x and implement the carrying of state when chaining static class methods by duplicating & nesting code. |
defaultOne: string, | ||
defaultTwo: number, | ||
version: string, | ||
}>({ ...BaseLevelTwo.defaultOptions }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why the destructoring? Isn't this the same as
}>({ ...BaseLevelTwo.defaultOptions }); | |
}>(BaseLevelTwo.defaultOptions); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ha, this one was nifty. tsd
sees an intersection type as not being the same as its equivalent solo type.
index.test-d.ts:86:0
✖ 56:0 Parameter type { defaultOne: string; defaultTwo: number; version: string; } is not identical to argument type { defaultOne: string; version: string; } & { defaultTwo: number; }.
I accidentally closed #59 by pushing it to the Josh wrote
I think what broke is the combination of Here is a complex test I'd like to see pass const BaseWithManyChainedDefaultsAndPlugins = Base.defaults({
defaultOne: "value",
})
.plugin(fooPlugin, barPlugin, voidPlugin)
.defaults({
defaultTwo: 0,
})
.plugin(withOptionsPlugin)
.defaults({
defaultThree: ["a", "b", "c"],
});
expectType<{
defaultOne: string;
defaultTwo: number;
defaultThree: string[];
}>({ ...BaseWithManyChainedDefaultsAndPlugins.defaultOptions });
const baseWithManyChainedDefaultsAndPlugins =
new BaseWithManyChainedDefaultsAndPlugins({
version: "1.2.3",
foo: "bar",
});
expectType<string>(baseWithManyChainedDefaultsAndPlugins.foo);
expectType<string>(baseWithManyChainedDefaultsAndPlugins.bar);
expectType<string>(baseWithManyChainedDefaultsAndPlugins.getFooOption()); Right now the last three A minimal test case to reproduce this problem is const BaseWithManyChainedDefaultsAndPlugins = Base.defaults({
defaultOne: "value",
})
.plugin(fooPlugin)
.defaults({
defaultTwo: 0,
});
const baseWithManyChainedDefaultsAndPlugins =
new BaseWithManyChainedDefaultsAndPlugins({
version: "1.2.3",
});
expectType<string>(baseWithManyChainedDefaultsAndPlugins.foo); Calling |
Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
T1 extends Plugin, | ||
T2 extends Plugin[] | ||
Class extends ClassWithPlugins, | ||
Plugins extends [Plugin, ...Plugin[]], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New little nifty improvement recently added to tuples in TypeScript ✨ no more multiple parameters!
version: "1.2.3", | ||
}); | ||
|
||
expectType<string>(baseWithChainedDefaultsAndPlugins.foo); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a tough one! It doesn't repro if either of the .defaults({ ... })
are removed above... Tricky!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got it! It was the this: Class
generic.
My leading hunch was that whatever information added the foo: string
from the .plugin(fooPlugin)
call was being abandoned by the .defaults
calls going on top of each other. That meant the piece of the type that contained foo
wasn't getting preserved.
That piece of type was the this: Class
-> & Class
. Using just one <Class>
generic from the start meant new type information on new this
scopes that had extra stuff added to them was being lost.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
just a question, looks great otherwise! Super cool that you got it all working!
Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oops sorry I forgot to submit my review yesterday
type ConstructorRequiringVersion<Class extends ClassWithPlugins, PredefinedOptions> = { | ||
defaultOptions: PredefinedOptions; | ||
} & (PredefinedOptions extends { version: string } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's okay to hard-code the required version right now, but we should make this flexible later.
The Base.Options
interface can be extended with other required options, and these should be taken into account. Also the version
is random and I just put it in here for exploring the blockers I ran into with Octokit, I don't think it makes sense to have any default options in the Base
class, they should all be added when using Base by extending the Base.Options
interface
I'll create a follow up issue, I think this PR is big enough as-is 😁
Base.defaults()
Base.defaults()
calls
🎉 This PR is included in version 3.3.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Base.defaults()
callsBase.defaults()
calls
Given that a
version
constructor parameter is required, the following code should work without type errors.