Skip to content

Request to expose zeroType emptyStringType and isTypeAssignableTo on the TS TypeCheckerΒ #50694

Closed
@sstchur

Description

@sstchur

Suggestion

Proposal to expose zeroType (as getZeroType()), emptyStringType (as getEmptyStringType()) and isTypeAssignableTo on the TS TypeChecker

πŸ” Search Terms

#zeroType, #emptyStringType, #isTypeAssignableTo

βœ… Viability Checklist

Searched issues for: zeroType, emptyStringType, isTypeAssignableTo
There are two open issues discussing the request to expose isTypeAssignableTo, but it appears to have fizzled out. Links here:
#11728 and #9879

My suggestion meets these guidelines:

  • This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • This wouldn't change the runtime behavior of existing JavaScript code
  • This could be implemented without emitting different JS based on the types of the expressions
  • This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

⭐ Suggestion

The TS TypeChecker already exposes getNullType(), getFalseType(), and getUndefinedType(). I'm using all of these in an ESLint rule I'm working on. However, I have no trivial way to get at the 0 type or the '' (empty string) type. The

The zeroType and the emptyStringType do indeed appear to exist in the TS code (checker.ts lines 1011 and 1012):

        const emptyStringType = getStringLiteralType("");
        const zeroType = getNumberLiteralType(0);

But they do not appear to be exposed like getNullType(), getUndefinedType(), and getFalseType().

Presently, I have a dirty dirty hack where I create a program in memory and yank out the types, as in:

function createZeroAndEmptyStringTypes(compilerOptions) {
  const code = `
    const zero: 0 = 0;
    const emptyString: '' = '';`;

  // NOTE: no files are actually written to disk, despite the name of this method.
  // This process happens entirely in memory.
  const sourceFile = ts.createSourceFile('doesnotmatter.ts', code, compilerOptions.target);

  const compilerHost = {
    getSourceFile: (name, languageVersion) => sourceFile,
    writeFile: (filename, data) => {},
    getDefaultLibFileName: () => 'lib.d.ts',
    useCaseSensitiveFileNames: () => false,
    getCanonicalFileName: (filename) => filename,
    getCurrentDirectory: () => '',
    getNewLine: () => '\n',
    getDirectories: () => [],
    fileExists: () => true,
    readFile: () => '',
    getCompilerOptions: () => compilerOptions
  };

  const program = ts.createProgram([sourceFile.fileName], compilerOptions, compilerHost);
  const checker = program.getTypeChecker();

  let zeroType, emptyStringType;
  ts.forEachChild(sourceFile, visit);
  return [zeroType, emptyStringType];

  function visit(node) {
    if (ts.isVariableDeclaration(node)) {
      const type = checker.getTypeAtLocation(node);
      const name = node.symbol?.escapedName;
      if (name === 'emptyString') {
        emptyStringType = type;
      } else if (name === 'zero') {
        zeroType = type;
      } else {
        throw new Error(`Unexpected symbol name: ${name} while creating falsy types of emptyString and zero`);
      }
    }
    ts.forEachChild(node, visit);
  }
}

Besides being pretty ugly, probably inefficient and just generally offending my sensibilities, this has some very practical drawbacks. For instance, any 0 type retrieved via getTypeAtLocation() from the actual code being linted, will not be equal to the poorly generated zeroType I create in my dirty function above (same for emptyStringType). In fact, even calling isTypeAssignableTo(zeroType, realZeroTypeFromLintedCode) will return false! I suspect there may be other cases as well, where is it unsafe/unreliable to use these generated types and expect them to behave in a reliable way within the program actually being linted.

πŸ“ƒ Motivating Example

The main motivation is to support an effort to create a new ESLint rule. Proposal for this rule is available here: typescript-eslint/typescript-eslint#5592

There you will find a (collapsed) list of ~50ish test cases that illustrate what the rule should do. These test cases are likely the most insightful bit of code in terms of understanding the spirit of the rule.

πŸ’» Use Cases

As the ESLint rule needs to identify cases where a type is assignable one (and only one) falsy value, exposing getZeroType() and getEmptyStringType() would eliminate the dirty code I posted above. Here is an snippet from the rule's code of how the type would be used:

    function isEligibleForShortening: (leftType, rightType) => {
      const rightSideIsFalsy = TSUtils.isFalsyType(rightType);
      const leftSideFalsies = [...falsies].filter((falsy) => checker.isTypeAssignableTo(falsy, leftType));
      const leftSideAcceptsExactlyOneFalsy = leftSideFalsies.length === 1;
      return (
        rightSideIsFalsy &&
        leftSideAcceptsExactlyOneFalsy &&
        leftSideFalsies[0] === rightType        // THIS IS NOT CURRENTLY POSSIBLE
      );
    }

In the above snippet, leftSideFalsies[0] === rightType does not always work. When rightType (which actually retrieved from the code being linted) is compared against a generated zeroType or emptyStringType, this check fails. To work around this, the actual code being used today is:

function areEffectivelyEqual(type1, type2) {
  return (
    type1 === type2 || (TSUtils.isLiteralType(type1) && TSUtils.isLiteralType(type2) && type1.value === type2.value)
  );
}

This appears to work (for my needs) but it gives me a similarly yucky feeling to the dirty function that generates the zeroType and the emptyStringType (which is the whole reason this hack is needed in the first place).

Metadata

Metadata

Assignees

No one assigned

    Labels

    APIRelates to the public API for TypeScriptIn DiscussionNot yet reached consensusSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions