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

docs: create coding standards doc #37700

Open
wants to merge 1 commit into
base: master
from

Conversation

@jelbourn
Copy link
Member

jelbourn commented Jun 24, 2020

Create initial document for Angular framework coding standards. This
document will evolve over time. This version contains all
non-controversial rules as discussed in a recent team meeting. Some text
and examples were copied from angular/components.

Create initial document for Angular framework coding standards. This
document will evolve over time. This version contains all
non-controversial rules as discussed in a recent team meeting. Some text
and examples were copied from angular/components.

##### Observables

Don't suffix observables with `$`.

This comment has been minimized.

Copy link
@CaerusKaru

CaerusKaru Jun 24, 2020

Member

Proponents of Polish notation everywhere weep

Prefer `for of` to `Array.prototype.forEach`. The `for of` construct makes debugging easier and may
reduce overhead from unnecessary function invocations (though browsers do generally optimize this
well).
Comment on lines +101 to +103

This comment has been minimized.

Copy link
@JoostK

JoostK Jun 24, 2020

Member

I would add an exception for hot code paths here, e.g. in the Ivy runtime only for (const i = 0; i < a.length; i++) is used when iterating arrays.

In general, maybe it would be a good idea to include a chapter for performance sensitive code, where some of these standards may not be desirable.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

👍
I would add another paragraph discussing for ... of vs "plain" for.

Consider private constructors for all new code. This prevents extension outside of the framework,
which is generally discouraged unless otherwise documented.
Comment on lines +146 to +147

This comment has been minimized.

Copy link
@JoostK

JoostK Jun 24, 2020

Member

This could use some samples as to what a private constructor is and how such classes have to be instantiated instead using named constructors in the form of static members.


##### Variable declarations

Prefer `const` wherever possible. Avoid `var` unless absolutely necessary.

This comment has been minimized.

Copy link
@JoostK

JoostK Jun 24, 2020

Member

I would mention let as the alternative for var, as currently this may be interpreted as var being the one alternative to const.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

Also, where would var be absolutely necessary? (I can't think of any case.)

This comment has been minimized.

Copy link
@dgp1130

dgp1130 Jun 24, 2020

Contributor

var is useful if you want to declare the value on the global scope:

// At root scope:
var foo = 1;
let bar = 2;
console.log(window.foo); // 1
console.log(window.bar); // undefined

I agree that this behavior is usually undesired in the first place and is very rarely actually useful. If this is ever needed there should be a sizeable comment next to it explaining exactly why it is necessary.

class InputWithNgModel { /* ... */ }
/** AVOID: does not fully describe the scenario under test. */

This comment has been minimized.

Copy link
@JoostK

JoostK Jun 24, 2020

Member

You don't want to look at the compiler tests, then :-)

#### Boolean arguments

Avoid adding boolean arguments to a method in cases where that argument means "do something extra".
In these cases, prefer breaking the behavior up into different functions.

This comment has been minimized.

Copy link
@JoostK

JoostK Jun 24, 2020

Member

An alternative alternative could be to have an enum to replace the boolean.

This comment has been minimized.

Copy link
@mhevery

mhevery Jun 24, 2020

Member

Could we add a caveat. I think the above is very true for public API, but for private(internal) API we often do boolean arguments for perf reasons and or size. We should make that distinction here as well.

}
function createTargetElement() {
// ...

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member
Suggested change
// ...
// ...
In TypeScript code, use JsDoc-style comments for descriptions (on classes, members, etc.) and
use `//` style comments for everything else (explanations, background info, etc.).

### API Design

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

Does this apply to Angular public API (i.e. stuff on our docs), public API of a class/module (i.e. public methods, exported functions, etc.) or any method/function?

Prefer `for of` to `Array.prototype.forEach`. The `for of` construct makes debugging easier and may
reduce overhead from unnecessary function invocations (though browsers do generally optimize this
well).

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

👍
I would add another paragraph discussing for ... of vs "plain" for.


Properties should have a concise description of what the property means:
```typescript
/** The label position relative to the checkbox. Defaults to 'after' */

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

Is the Defaults to part needed when the default value is obvious in TS?

* Opens a modal dialog containing the given component.
* @param component Type of the component to load into the dialog.
* @param config Dialog configuration options.
* @returns Reference to the newly-opened dialog.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

We need a rule that says it must be @returns and not @return. At all const 😛


##### Variable declarations

Prefer `const` wherever possible. Avoid `var` unless absolutely necessary.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

Also, where would var be absolutely necessary? (I can't think of any case.)

##### General

* Prefer writing out words instead of using abbreviations.
* Prefer *exact* names over short names (within reason). E.g., `labelPosition` is better than

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member
const myCoolComponentFactoryResolvedComponentFactory = componentFactoryResolver.resolveComponentFactory(myCoolComponentFactoryResolvedComponent);

😛


##### Observables

Don't suffix observables with `$`.

This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member

FWIW, I don't like this rule 😢

This comment has been minimized.

Copy link
@spock123

spock123 Jun 24, 2020

Very surprised seeing this as well.
I was under impression that was a good practise.
I'm definitely fond of it.

class InputComp { /* ... */ }
```


This comment has been minimized.

Copy link
@gkalpak

gkalpak Jun 24, 2020

Member
Suggested change
##### Test names

Use descriptive names for jasmine tests. Ideally, test names should read as a sentence, often of
the form "it should...".

This comment has been minimized.

Copy link
@gkalpak
#### Boolean arguments

Avoid adding boolean arguments to a method in cases where that argument means "do something extra".
In these cases, prefer breaking the behavior up into different functions.

This comment has been minimized.

Copy link
@mhevery

mhevery Jun 24, 2020

Member

Could we add a caveat. I think the above is very true for public API, but for private(internal) API we often do boolean arguments for perf reasons and or size. We should make that distinction here as well.

#### Optional arguments

Use optional function arguments only when such an argument makes sense for an API. Don't use
optional arguments merely for convenience in implementation.

This comment has been minimized.

Copy link
@mhevery

mhevery Jun 24, 2020

Member

NOTE: under some circumstances (When the method can't be inlined, the VM needs to create a trampoline with matching argument sets), option arguments have negative perf impact.

#### Getters and Setters

* Only use getters and setters for `@Input` properties or when otherwise required for API
compatibility.

This comment has been minimized.

Copy link
@mhevery

mhevery Jun 24, 2020

Member
Suggested change
compatibility.
compatibility. (Using getters/setters implies side-effect, and creates a higher cognitive load to the reader. Use only when it is necessary.)
Prefer `for of` to `Array.prototype.forEach`. The `for of` construct makes debugging easier and may
reduce overhead from unnecessary function invocations (though browsers do generally optimize this
well).

#### Try-Catch

Avoid `try-catch` blocks, instead preferring to prevent an error from being thrown in the first

This comment has been minimized.

Copy link
@mhevery

mhevery Jun 24, 2020

Member

Not sure I agree, perhaps justification as to what you are trying to avoid would be helpfull. Why is try-catch bad?


#### Try-Catch

Avoid `try-catch` blocks, instead preferring to prevent an error from being thrown in the first

This comment has been minimized.

Copy link
@dgp1130

dgp1130 Jun 24, 2020

Contributor

Not to start a bikeshedding war on code style, but the one advantage of throwing errors is that they can contain string messages about what went wrong which can be easily rethrown and composed to maintain useful error messages.

For example, consider the error-less implementation:

function computeAThing(): number|null {
  const firstResult = computeFirstPiece();
  if (!firstResult) return null;

  const secondResult = computeFirstPiece();
  if (!secondResult) return null;

  return firstResult + secondResult;
}

console.log(computeAThing()); // If this logs `null`, why?

There is no way to know which of the two pieces went wrong. Alternatively consider:

function computeAThing(): number {
  let firstResult: number;
  try {
    firstResult = computeFirstPiece();
  } catch (err) {
    throw new Error(`Failed to compute the first piece: ${err.message}`);
  }

  let secondResult: number;
  try {
    secondResult = computeSecondPiece();
  } catch (err) { 
    throw new Error(`Failed to compute the second piece: ${err.message}`);
  }

  return firstResult + secondResult;
}

try {
  console.log(computeAThing());
} catch (err) {
  console.log('Failed to compute a thing: ${err.message}');
}

The terrible try-catch syntax aside, this has the advantage of more accurately communicating exactly what went wrong at any given point in a computation with composable error messages giving full context about the operation. It also allows types to be much clearer; because computeAThing() always returns a number there is no ambiguity as to what null represents. Errors naturally propagate as well, so omitting a try-catch at one level of abstraction will still be caught at a higher level of abstraction.

I do agree that Error in JavaScript is not ideal. You can't wrap an Error like in other languages, you just have to throw a new Error with a concatenated message. try-catch syntax is awkward due to scoping and often requires a mutable let. It's also a bit of a pain to disambiguate different kinds of errors (though whether that is a good idea is a separate discussion).

My main point here is just that I don't feel we should outright ban them when we lack effective alternative options to fill that need. A contextual error message can be incredibly helpful for debugging problems.

A couple notable alternatives:

"Return an abstract data type (ADT)": As much as I like this pattern in general, I don't think it can be used in public-facing APIs because anyone using them in TypeScript can use an exhaustive switch statement, which means that adding a new failure state is actually a breaking change. Exhaustive switch is a useful feature, but most developers don't see this caveat and tend to misuse it as a result.

"Use a Result monad" (a la Result<T, Err> in Rust or Maybe is Haskell): Personally I think this an ideal solution but it just isn't well supported in the JavaScript ecosystem. We'd really need a standardized form of the type to be able to use it consistently and effectively.


##### Variable declarations

Prefer `const` wherever possible. Avoid `var` unless absolutely necessary.

This comment has been minimized.

Copy link
@dgp1130

dgp1130 Jun 24, 2020

Contributor

var is useful if you want to declare the value on the global scope:

// At root scope:
var foo = 1;
let bar = 2;
console.log(window.foo); // 1
console.log(window.bar); // undefined

I agree that this behavior is usually undesired in the first place and is very rarely actually useful. If this is ever needed there should be a sizeable comment next to it explaining exactly why it is necessary.

```


#### RxJS

This comment has been minimized.

Copy link
@dgp1130

dgp1130 Jun 24, 2020

Contributor

This may not belong in style guide, but I'll put it here just in case:

One rule I always use is to prefer defer() over from() when translating a Promise into an Observable. Consider:

declare function doAsyncThing(): Promise<string>;

function doObservableThing(): Observable<string> {
  return from(doAsyncThing()); // doAsyncThing() is immediately invoked.
}

vs:

declare function doAsyncThing(): Promise<string>;

function doObservableThing(): Observable<string> {
  return defer(() => from(doAsyncThing())); // doAsyncThing() is invoked on `subscribe()`
}

You almost always want the latter option, so I'd love to see a little more awareness about this particular foot-gun. Though a style guide may not be the best place to put it.

```

#### Testing

This comment has been minimized.

Copy link
@AndrewKushnir

AndrewKushnir Jun 24, 2020

Contributor

Just as an idea/proposal: include human-language description of the test in case it has some non-trivial setup or scenario (in a trivial cases it's typically excessive). For example:

it('should calculate transitive scopes when overrides are present', () => {
  // Set up an NgModule hierarchy with two modules, A and B, each with their own component.
  // Module B additionally re-exports module A. Also declare two mock components which can be
  // used in tests to verify that overrides within this hierarchy are working correctly.
  TestBed.configureTestingModule({ ... });
  ...
});

This is actually @alxhub's idea that he mentioned during the code review for one of my PRs in
#36649 (comment).

Copy link
Contributor

kapunahelewong left a comment

This is great! So excited to see how it comes together. I peeked out of curiosity and just noticed a few little things. I'm happy to do a more thorough docs review down the road too, if you'd like.


## Code practices

#### Write useful comments

This comment has been minimized.

Copy link
@kapunahelewong

kapunahelewong Jun 25, 2020

Contributor

Did you mean ### here?

##### General

* Prefer writing out words instead of using abbreviations.
* Prefer *exact* names over short names (within reason). E.g., `labelPosition` is better than

This comment has been minimized.

Copy link
@kapunahelewong

kapunahelewong Jun 25, 2020

Contributor

To avoid Latin abbreviations (and confusion), we've been changing them to their English equivalents: e.g. --> for example

* Prefer writing out words instead of using abbreviations.
* Prefer *exact* names over short names (within reason). E.g., `labelPosition` is better than
`align` because the former much more exactly communicates what the property means.
* Except for `@Input` properties, use `is` and `has` prefixes for boolean properties / methods.

This comment has been minimized.

Copy link
@kapunahelewong

kapunahelewong Jun 25, 2020

Contributor
Suggested change
* Except for `@Input` properties, use `is` and `has` prefixes for boolean properties / methods.
* Except for `@Input()` properties, use `is` and `has` prefixes for boolean properties / methods.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked issues

Successfully merging this pull request may close these issues.

None yet

10 participants
You can’t perform that action at this time.