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

Enable @typescript-eslint/non-nullable-type-assertion-style #348

Open
MajorLift opened this issue Feb 29, 2024 · 0 comments
Open

Enable @typescript-eslint/non-nullable-type-assertion-style #348

MajorLift opened this issue Feb 29, 2024 · 0 comments

Comments

@MajorLift
Copy link

MajorLift commented Feb 29, 2024

Motivation

We currently ban non-nullable assertions (!) completely with the ESLint rule @typescript-eslint/no-non-null-assertion.

However, there are arguments for using ! assertions as a tool for improving type safety.

Explanation

The ESLint rule @typescript-eslint/non-nullable-type-assertion-style provides alerts when a as assertion on a nullable type could be replaced with a ! instead.

Enabling this rule wouldn't require disabling @typescript-eslint/no-non-null-assertion. We could still mandate ESLint disable comments when using ! to make it clear that they should be used with caution and as an exception rather than the norm.

In defense of !

1. ! is safer than as

! non-null assertions are generally preferred for requiring less code and being harder to fall out of sync as types change.
Source: https://typescript-eslint.io/rules/non-nullable-type-assertion-style/

as enforces a positive definition that specifies what a type is, while ! applies a negative definition, specifying what a type isn't.

In other words, as prevents any type other than the asserted one from being applied, even if a more accurate one could be inferred due to code changes. ! doesn't share this brittleness, as it allows any other type to be inferred in place of the existing one, only stepping in to strip | undefined, | null from the type.

For this reason, replacing as with ! where applicable may represent a net improvement for type safety.

const example: string[] | undefined = ...
const bad = (example as string[]).pop();
const good = example!.pop();

2. TypeScript inference on nullability is limited

Some examples where the code guarantees the non-nullability of a variable, but TypeScript fails to infer this fact.

  1. Optional chaining
while (queue[0]?.origin === originOfCurrentBatch) {
  // `queue` and its head element are defined
  const nextEntry = queue.shift()!;
}

while (queue.length > 0) {
  // `queue` and its head element are defined
  const nextEntry = queue.shift()!;
}
  1. Array index access
if (chainIds.length > NETWORK_CACHE_LIMIT.MAX) {
  const oldestChainId = chainIds.sort(
    (c1, c2) =>
      Number(this.state.chainStatus[c2]?.lastVisited) -
      Number(this.state.chainStatus[c1]?.lastVisited),
  )[NETWORK_CACHE_LIMIT.MAX];
  // `oldestChainId` will always be defined, as `chainIds` is guaranteed to have at least `NETWORK_CACHE_LIMIT.MAX` elements
  oldChainIds.push(oldestChainId!);
}
  1. Index signature
const oldChainIds = Object.keys(chainIds).filter(
  (chainId) =>
    // `chainId` is of type `keyof typeof chainIds`, meaning `chainIds[chainId]` must be defined
    chainIds[chainId]!.lastVisited < currentTimestamp - NETWORK_CACHE_DURATION,
);
  1. Assertions made in a different conditional branch
/*
 * @param options.currentVersion - The current version. Required if
 * `isReleaseCandidate` is set, but optional otherwise.
 */
export async function updateChangelog({
  currentVersion,
  isReleaseCandidate,
  tagPrefixes = ['v'],
}: {
  currentVersion?: string;
  isReleaseCandidate: boolean;
  tagPrefixes: [string, string[]];
}) {
  if (isReleaseCandidate && !currentVersion) {
    throw new Error(
      `A version must be specified if 'isReleaseCandidate' is set.`,
    );
  }
  // Due to the first null check, if `isReleaseCandidate` is truthy, `currentVersion` should always be truthy
  if (
    isReleaseCandidate &&
    await getMostRecentTag({ tagPrefixes }) === `${tagPrefixes[0]}${currentVersion!}` // `currentVersion` is inferred as 'string | undefined'
  ) {
    throw new Error(
      `Current version already has tag, which is unexpected for a release candidate.`,
    );
  }
...
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant