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

Clean-up foo: SomeType | undefined vs foo?: SomeType #124362

Closed
jrieken opened this issue May 21, 2021 · 16 comments
Closed

Clean-up foo: SomeType | undefined vs foo?: SomeType #124362

jrieken opened this issue May 21, 2021 · 16 comments
Assignees
Labels
api debt Code quality issues
Milestone

Comments

@jrieken
Copy link
Member

jrieken commented May 21, 2021

When defining optional values in object we generally prefer the question-mark notation but it seems that we aren't consistent with this. Ideally, we always use one way, min-bar should be to consistency per type, not like this

vscode/src/vs/vscode.d.ts

Lines 5600 to 5611 in 4c1474b

/**
* The priority of this item. Higher value means the item should
* be shown more to the left.
*/
readonly priority?: number;
/**
* The name of the entry, like 'Python Language Indicator', 'Git Status' etc.
* Try to keep the length of the name short, yet descriptive enough that
* users can understand what the status bar item is about.
*/
name: string | undefined;

@jrieken jrieken added api debt Code quality issues labels May 21, 2021
@jrieken
Copy link
Member Author

jrieken commented May 21, 2021

Might also be worth to add a API-lint rule for this

@mjbvz
Copy link
Contributor

mjbvz commented May 21, 2021

These have subtly different meanings:

  • property?: type — You can simply omit the property / argument. You can also pass in undefined if you want to be explicit

  • property: type | undefined — The must explicitly specify the property or argument by setting it to undefined

We can review places where these occur in our d.ts, but coming up with a lint rule for this is probably not possible

@mjbvz
Copy link
Contributor

mjbvz commented Aug 6, 2021

With TS 4.4, there's another complication: Exact Optional Property Types

This means that ? is no longer automatically equivalent to | undefined

@mjbvz
Copy link
Contributor

mjbvz commented Oct 7, 2021

Here's a draft of guidance around this:

For function/method parameters:

  • For implementation, treat omitting a parameter with ? the same as explicitly passing in undefined
  • Use | undefined when you want to callers to always have to consider the parameter.
  • Use ? when you want to allow callers to omit the parameter.
  • Never use ? and | undefined on a parameter. Instead follow the two rules above to decide which version to use
  • If adding a new parameter to an existing function, use ? as this allows the new signature to be backwards compatible with the old version
  • Do not add an overload to add an optional parameter to the end of the function. Instead use ?

For properties

  • Do not write code that treats the absence of a property differently than a property being present but set to undefined

    • This can sometimes hit you on spreads or iterating through objects, so just something to be aware of
  • For readonly properties on interfaces that VS Code exposes to extensions (this include managed objects, as well as the objects passed to events):

    • Use | undefined as this makes it clear the property exists but has the value undefined.
  • For readonly properties on options bag type objects passed from extensions to VS Code:

    • Use ? when it is ok to omit the property
    • Use | undefined when you want the user to have to pass in the property but undefined signals that you will fall back to some default
    • Try to avoid ? + | undefined in most cases. Instead use ?. Using both ? + | undefined isn't wrong, but it's often more clear to treat omitting the property as falling back to the default rather than passing in undefined
  • For unmanaged, writable objects:

    • If using ?, always also add | undefined unless want to allow the property to be omitted during initialization, but never allow users to explicitly set it to undefined afterwards. I don't think we have many cases where this will be needed
      • In these cases, you may want to try changing the api to avoid this potential confusion
    • If adding a new property to an unmanaged object, use ? as this ensures the type is backwards compatible with the old version

mjbvz added a commit that referenced this issue Oct 7, 2021
…cts that we control

For #124362

This includes:

- Event objects
- Context objects passed to providers
- Managed objects such as `TextEditor`
@mjbvz mjbvz added this to the Backlog milestone Oct 7, 2021
@jrieken jrieken modified the milestones: Backlog, October 2021 Oct 11, 2021
@mjbvz
Copy link
Contributor

mjbvz commented Oct 22, 2021

For November, let's review classes that use readonly property?: type. I think almost all of these should be changed to readonly property: type | undefined instead because this makes it more clear that the property exists but is set to undefined.

Assigning owners per class:

Let me know if you have any questions about fixing these cases

@mjbvz mjbvz modified the milestones: October 2021, November 2021 Oct 22, 2021
alexdima added a commit that referenced this issue Oct 22, 2021
bpasero added a commit that referenced this issue Oct 22, 2021
connor4312 added a commit that referenced this issue Oct 22, 2021
alexr00 added a commit that referenced this issue Nov 10, 2021
@alexr00 alexr00 removed their assignment Nov 10, 2021
@alexdima alexdima removed their assignment Nov 10, 2021
@alexdima
Copy link
Member

@mjbvz IIRC I did mine a couple of weeks ago. Is there something more to do?

@weinand weinand assigned aeschli and unassigned weinand Nov 10, 2021
@aeschli
Copy link
Contributor

aeschli commented Nov 10, 2021

With exact-optional-property-types extension authors will have to provide all properties of type readonly property: type | undefined.
Wouldn't that be a breaking change?

I think it makes sense to keep readonly property?: type for optional properties.

@aeschli aeschli removed their assignment Nov 10, 2021
@weinand
Copy link
Contributor

weinand commented Nov 10, 2021

Since the constructors of all my assigned classes are public, changing readonly property?: type into readonly property: type | undefined would be breaking. So no changes on my side.

@mjbvz
Copy link
Contributor

mjbvz commented Nov 10, 2021

@aeschli Not if the type is maintained by VS Code. It would be breaking if you make this change to an options bag object

@weinand You should not change the constructor arguments but change the property. For example, DebugAdapterServer

	export class DebugAdapterServer {
		readonly port: number;
		readonly host?: string;
		constructor(port: number, host?: string);
	}

Should become:

	export class DebugAdapterServer {
		readonly port: number;
		readonly host: string | undefined;
		constructor(port: number, host?: string);
	}

Using | undefined is more consistent in this case and also matches how these classes are implemented:

@mjbvz
Copy link
Contributor

mjbvz commented Nov 10, 2021

@weinand and @aeschli Can you please quickly revisit your classes and update any cases that are similar to the above example

@weinand
Copy link
Contributor

weinand commented Nov 10, 2021

@mjbvz sorry Matt that I did not made myself clear:
since the constructor is not private, objects of class DebugAdapterServer can be created as { port: 1234 } (which is actually done in some extensions). Changing host?: string to host: string | undefined breaks that code.
This is true for all my other cases...

@aeschli
Copy link
Contributor

aeschli commented Nov 10, 2021

@mjbvz Same for ThemeIcon, const themeIcon : ThemeIcon = { id: 'zap' }; would no longer compile.
If we don't encourage creating objects like that, I'll make the change (or feel free to make it yourself)

@mjbvz
Copy link
Contributor

mjbvz commented Nov 11, 2021

Thanks for the additional context

With typescript in general, if something is typed as class, we expect instances of that class. If you only care about the properties, use interface instead. However we do often use classes + constructors as quick ways to create simple objects with a few properties in vscode.d.ts, so we are not following these rules all the time

A few options for these cases:

  1. If you want extensions to always create an instance of the class, change the properties to readonly prop: type | undefined

  2. If you instead want to allow extensions to pass in objects with the same shape as the class, change the property on the class to: readonly prop?: type | undefined. That is ugly but I think makes the types correct for strictOptionalProperties

These change will not break the runtime behavior of extensions. However option 1 may break compilation for extensions that don't create instances of the class

@aeschli
Copy link
Contributor

aeschli commented Nov 11, 2021

Thanks @mjbvz for looking into this and coming up with guidelines.
I went with color?: ThemeColor | undefined; for ThemeIcon as I expect some extensions to use the { id: 'zap' } notation. We happen to do it in our sample.
For LinkedEditingRanges I went with the recommended way.
Feel free to change this if you prefer to have this consistent.

@weinand
Copy link
Contributor

weinand commented Nov 11, 2021

@mjbvz thanks for your suggestions.

Since option 1 is breaking, we cannot use it (even if we would like to encourage users to always create class instances). So I've applied option 2 in all my cases.

@mjbvz
Copy link
Contributor

mjbvz commented Nov 16, 2021

Thanks. I'm going to close this and move the API guidance to a wiki

I believe vscode.d.ts mostly gets strictOptionalProperties correct now. We can handle any other issues with it on a case by case basis since making a complete and thorough pass through the api doesn't seem worthwhile at this time

@mjbvz mjbvz closed this as completed Nov 16, 2021
@github-actions github-actions bot locked and limited conversation to collaborators Dec 31, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api debt Code quality issues
Projects
None yet
Development

No branches or pull requests

6 participants