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

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

Closed
5 tasks done
sstchur opened this issue Sep 8, 2022 · 3 comments · Fixed by #52473
Closed
5 tasks done
Labels
API Relates to the public API for TypeScript In Discussion Not yet reached consensus Suggestion An idea for TypeScript

Comments

@sstchur
Copy link
Contributor

sstchur commented Sep 8, 2022

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).

@andrewbranch
Copy link
Member

It seems like getStringLiteralType and getNumberLiteralType belong in this block

/* @internal */ getAnyType(): Type;
/* @internal */ getStringType(): Type;
/* @internal */ getNumberType(): Type;
/* @internal */ getBooleanType(): Type;
/* @internal */ getFalseType(fresh?: boolean): Type;
/* @internal */ getTrueType(fresh?: boolean): Type;
/* @internal */ getVoidType(): Type;
/* @internal */ getUndefinedType(): Type;
/* @internal */ getNullType(): Type;
/* @internal */ getESSymbolType(): Type;
/* @internal */ getNeverType(): Type;
/* @internal */ getOptionalType(): Type;
/* @internal */ getUnionType(types: Type[], subtypeReduction?: UnionReduction): Type;
/* @internal */ createArrayType(elementType: Type): Type;
/* @internal */ getElementTypeOfArrayType(arrayType: Type): Type | undefined;
/* @internal */ createPromiseType(type: Type): Type;
/* @internal */ getPromiseType(): Type;
/* @internal */ getPromiseLikeType(): Type;

@DanielRosenwasser what do you think?

@andrewbranch andrewbranch added Suggestion An idea for TypeScript In Discussion Not yet reached consensus API Relates to the public API for TypeScript labels Sep 12, 2022
@sstchur
Copy link
Contributor Author

sstchur commented Sep 12, 2022

Oooo! I think exposing getStringLiteralType and getNumberLiteralType is a great idea! I think this better than my suggestion because it covers my use-case but is even more flexible, allowing consumers to generate whatever string or number literal types they may need.

@sstchur
Copy link
Contributor Author

sstchur commented Jan 20, 2023

Curious if any more thought has been given to this? Just today, something came up for me again where having this would have definitely made my life easier.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
API Relates to the public API for TypeScript In Discussion Not yet reached consensus Suggestion An idea for TypeScript
Projects
None yet
2 participants