-
Notifications
You must be signed in to change notification settings - Fork 13.2k
Description
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).