-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(context): add optional typing for Binding #1174
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
Conversation
aaddd7e to
6ebff6e
Compare
bajtos
left a comment
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.
IIUC, this pull request is introducing a subset of changes I proposed in my #1169. That's cool! Would you mind to list me as a coauthor of these changes? See https://github.com/blog/2496-commit-together-with-co-authors
I have few comments to consider/address, see below.
packages/context/src/context.ts
Outdated
|
|
||
| if (isPromiseLike(boundValue)) { | ||
| return boundValue.then(v => getDeepProperty(v, path) as T); | ||
| return boundValue.then(v => getDeepProperty<T>(v, path)); |
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.
Can you use a different generic parameter name than T please? I find it confusing to have Binding<T>#getValueOrPromise<T>, where each T is a different generic parameter.
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.
Well, did you notice that getValueOrPromise() is a method of Context, not Binding?
| * @param path Path to the property | ||
| */ | ||
| export function getDeepProperty(value: BoundValue, path: string): BoundValue { | ||
| export function getDeepProperty<T = BoundValue>( |
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.
While you are changing the signature of this function, would it make sense to give value argument a generic type too?
export function getDeepProperty<V, T = BoundValue>(
value: V,
path: string
): T {
// impl
}Ideally, I'd like to get rid of as many usages of any type as we can, eventually removing BoundValue alias too.
Feel free to leave such change out of this pull request if you disagree or if it's not trivial to make.
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.
I have struggled between option A and B:
A:
export function getDeepProperty<V = BoundValue, T = BoundValue>(
value: V,
path: string,
): T | undefined {// ...}B:
export function getDeepProperty<T = BoundValue, V = BoundValue>(
value: V,
path: string,
): T | undefined {// ...}For A, type V and T follows the convention as parameter and return types. But T is more important than V. With A, we cannot use getDeepProperty<string> anymore if the result is expected to string. Instead, we have to use getDeepProperty<V, string>. It's a bit awkward to me.
For B, T comes before V and it's not so natural either.
Anyhow, getDeepProperty is mostly used internally. So I go with A for now.
| it('gets the root value with a path', () => { | ||
| const obj = {x: {y: 1}}; | ||
| expect(getDeepProperty(obj, 'x.y')).to.eql(1); | ||
| expect(getDeepProperty<number>(obj, 'x.y')).to.eql(1); |
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.
I think the changes in this files should not be strictly required, because getDeepProperty defaults to any type, which is fine as long as expect is concerned.
Are you adding <number> specialization to verify that getDeepProperty accepts a generic parameter? We should write a new test to do so, one that will also verify at compile time that the result of getDeepProperty is indeed the type we specified. (Most of) the existing tests should remain unchanged, that way we can verify that getDeepProperty can be used without the generic parameter too.
See #1169 for an example:
To make the intent easier to understand without the need for excessive comments, I would write a helper along the following lines:
// usage
it('allows access to a deep property', async () => {
const key = new BindingKey<object>('foo');
ctx.bind(key).to({rest: {port: 80}});
const value = await ctx.get(key.deepProperty<number>('rest.port'));
expectNumberAtCompileTime(value, 80);
});
// impl
function expectNumberAtCompileTime(actual: number, expected: number) {
expect(actual).to.be.a.Number()
expect(actual).to.equal(expected);
}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.
Fixed.
c0e239b to
d05c0ef
Compare
|
@bajtos PTAL |
d05c0ef to
6498ced
Compare
I am not sure that I understand what you are saying, the generic parameter names In my experience, it's ok to put the return parameter as the first generic argument, and then input parameter 1,2,3, etc. as following generic arguments. I find it pretty natural, because the type of the input arguments is know and can be inferred by the compiler, while the return type must be usually explicitly provided by the user. I have definitely seen this convention in code in the past. Another point to consider - the compiler should be able to infer the type of the input value for us, users should not need to provide it. For me, this would be the ideal signature - although I don't have bandwidth now to try it out: export function getDeepProperty<Result, Input>(
value: Input,
path: string,
): Result | undefined {// ...}
// intended usage:
getDeepProperty<number>({port: 80}, 'port');Anyhow, I agree |
bajtos
left a comment
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.
LGTM, but please consider renaming V and T in getDeepProperty and switching their order per my comment above.
|
If I understand correctly, this PR will be replacing #1169 right? |
6498ced to
e07ea83
Compare
Co-authored-by: Miroslav Bajtoš <mbajtoss@gmail.com>
e07ea83 to
e922eff
Compare
Fixed. |
| const ownerCtx = ctx.getOwnerContext(this.key); | ||
| if (ownerCtx && this._cache.has(ownerCtx)) { | ||
| return this._cache.get(ownerCtx); | ||
| return this._cache.get(ownerCtx)!; |
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.
I may have asked this before but what is the purpose of the trailing !?
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.
The ! tells TS compiler that the value is not undefined. In this case, we already test the key exists in the cache 1st.
This PR adds optional typing for
BindingandgetDeepProperty.Related to #1169
Checklist
npm testpasses on your machinepackages/cliwere updatedpackages/example-*were updated