Skip to content

[labs/context] Make @provide and @consume support optional and private properties.#3733

Merged
rictic merged 12 commits intolit:mainfrom
jpzwarte:fix/context-provide-optional-property
May 31, 2023
Merged

[labs/context] Make @provide and @consume support optional and private properties.#3733
rictic merged 12 commits intolit:mainfrom
jpzwarte:fix/context-provide-optional-property

Conversation

@jpzwarte
Copy link
Copy Markdown
Contributor

Fixes #3732

As far as I can tell, the change just works. All tests still pass.

@jpzwarte jpzwarte requested a review from benjamind as a code owner March 16, 2023 14:18
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 16, 2023

🦋 Changeset detected

Latest commit: 31096c7

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@lit-labs/context Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@jpzwarte jpzwarte requested a review from justinfagnani as a code owner March 16, 2023 14:21
@jpzwarte jpzwarte changed the title Make @provide also support optional properties. [labs/context] Make @provide also support optional properties. Mar 17, 2023
@justinfagnani
Copy link
Copy Markdown
Collaborator

I think the reason I didn't make this change when I did the change for @consume() is that there's the question of whether this provides undefined for a context key that can't be undefined.

const fooContext = createContext<string>('foo');

class Parent extends LitElement {
  @provide({context: fooContext})
  foo?: Foo;
  
  render() { return html`<x-child></x-child>`; }
}

class Child extends LitElement {
  @consume({context: fooContext})
  foo?: Foo;
}

In this case because we have a context of a string and not a string | undefined, if would be an error for this @provide/foo to provide the value.

But that's difficult to differentiate from this case:

const fooContext = createContext<string | undefined>('foo');

class Parent extends LitElement {
  @provide({context: fooContext})
  foo?: Foo;

where it would be ok to provide a value of undefined.

So I think that either:

  1. we can't tell whether to provide a value in this case and we need to keep the current requirement that the field is defined and matches the context type (which you need to include undefined if you want that) and we always provide a value, or
  2. we add a test to this suite that ensures that the consumer gets a number and not undefined, even if we don't set a value like in the other tests. I think we want that test anyway to nail down the intended behavior here.

@rictic
Copy link
Copy Markdown
Collaborator

rictic commented May 24, 2023

I think we can use a type like this to distinguish between the case where the context allows undefined (in which case an optional field is fine) from the case where it isn't.

type LooseRecord<K, V> = 
    V extends undefined ? 
        Partial<Record<K, V>> : 
        Record<K, V>;

I'll try it out and add some tests

@rictic rictic requested a review from kevinpschaaf as a code owner May 26, 2023 23:30
@rictic
Copy link
Copy Markdown
Collaborator

rictic commented May 26, 2023

This ended up turning into a slightly bigger change, but I added support for providing and consuming optional properties (with correct type checking), and private/protected properties (with no type checking), and with no changes to the authoring experience.

Also added type checking tests using // @ts-expect-error, as well as tests of our behavior when providing and consuming optional properties.

@rictic rictic changed the title [labs/context] Make @provide also support optional properties. [labs/context] Make @provide and @consume support optional and private properties. May 26, 2023
@rictic
Copy link
Copy Markdown
Collaborator

rictic commented May 26, 2023

Fixes #3883

@jpzwarte
Copy link
Copy Markdown
Contributor Author

@rictic Thanks for this!

@rictic rictic requested a review from AndrewJakubowicz May 30, 2023 16:56
Copy link
Copy Markdown
Contributor

@AndrewJakubowicz AndrewJakubowicz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow! There are some advanced types on display here.

TIL: about how to get around distributive conditional type behavior using square brackets!

Great use of type only tests which are necessary with such a complex change. The two types: FieldMustMatchProvidedType and FieldMustMatchContextType are very similar. Is it possible (or worthwhile) refactoring them into a single type?


Improve type checking of @provide and @consume. We now support optional properties, and public properties should be fully type checked.

This also adds support for using @provide and @consume with protected and private fields, but be aware that do to limitations in TypeScript's experimental decorators, the connection between the context and the property can not be type checked. We'll be able to fix this when standard decorators are released (current ETA TypeScript 5.2 in August 2023).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nits:

  • Code blocks around @provide and @consume.
  • Typo: "but be aware that do due to limitations"
  • Is there an issue tracking fixing this when standard decorators are released?

This info should also be added to https://lit.dev/docs/data/context/#context-type-checking

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • Done
  • Done
  • Added!

And sent over as lit/lit.dev#1134


Note that ContextRoot uses [WeakRefs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WeakRef) which are not supported in IE11.

### Protected / Private Properties
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth mentioning that standard private fields do not work (If I understand correctly)?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

assert.strictEqual(consumer.optionalValue, undefined);
assert.strictEqual(consumer.consumeOptionalWithDefault, undefined);
provider.optionalValue = 500;
await provider.updateComplete;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this be removed? - noting because in the test above only the consumer is awaited.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually both awaits can be removed. Done

}
}

suite('compilation tests', () => {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No action required - is it possible to add a failing compilation test with a standard private field? Maybe linked to a TODO with issue?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done!

Comment on lines +18 to +21
// this code doesn't need to run, it's tested by tsc
if (true as boolean) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clever way to skip everything.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

:ratjam:

Obj,
Key extends PropertyKey,
ProvidedType,
ReturnValue
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If ReturnValue is always void | any, is it something that can be hoisted out? E.g. type ReturnValue = void | any? Otherwise I expected it to possibly be parameterized.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, removed

: // Next we check whether the object has the property as an optional field
Obj extends Partial<Record<Key, infer Providing>>
? // Check assignability again. Note that we have to include undefined
// here on the consuming type because it's optional.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: Should this comment reference the providing type?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch!

context: ContextType;
consuming: Providing | undefined;
}
: // Ok, the field isn't present, so either someone's using consume
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "using consume provide"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, right! Done.

This was referenced May 31, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[labs/context] @provide forces a non-optional property

4 participants