Skip to content

Instantly share code, notes, and snippets.

@aztack
Forked from hackape/checker_mod.ts
Created July 26, 2021 03:09
Show Gist options
  • Save aztack/e938c319c6c8130eac5df43fc2e978d3 to your computer and use it in GitHub Desktop.
Save aztack/e938c319c6c8130eac5df43fc2e978d3 to your computer and use it in GitHub Desktop.
Answer to Stack Overflow Question: "How can I see how TypeScript computes types?"

instructions

  1. Clone the typescript repo (tips: git clone --depth=<x>, otherwise takes forever).
    Modify checker.ts, mainly involve the getConditionalType() function, see attached file below. I apply my mod to commit ba5e86f1406f39e89d56d4b32fd6ff8de09a0bf3, mind the difference of line number if you cloned a different revision.
    Then follow the repo's readme to compile a custom build.

  2. Download the tool.js file, then node --inspect to start a interactive shell.
    I recommend using the nodejs-v8-inspector-manager chrome extension to view result inside web console instead of terminal for readability. read on for usage example.

usage example in node interactive shell

Take for example the attached demo.ts file.

const checkAgainst = require('./tool.js');
var results = checkAgainst('/path/to/demo.ts', 171 /* position of `shouldBeNull` in `demo.ts` */, {});
var result = results.pop()

result explained

Now if you checkout result in console, you'll see it's basically a nested array that goes like:
[ [number, number, boolean], boolean, null ]

I made this array a tuple of [checkType, extendsType, trueType, falseType, bothTrueAndFalseType, undeterminedConditionalType]. If encountered a nested tuple, that means there's another chained conditional type resolution happening along the way. (For detail, look for pushTop() in attached checker_mod.ts.)

So the above result explained in plain english:

  1. the conditionalTypeA's checkType is another conditionalTypeB
  2. conditionalTypeB's checkType is number, it checks agains extendsType number and resolves to trueType boolean
  3. that make conditionalTypeA's checkType to be boolean, it checks agains extendsType boolean and resolves to trueType null

this is part of an answer to stack overflow question: https://stackoverflow.com/questions/58565584/how-can-i-see-how-typescript-computes-types

// https://github.com/microsoft/TypeScript/blob/ba5e86f1406f39e89d56d4b32fd6ff8de09a0bf3/src/compiler/checker.ts
// 1. add this line to ln:3
export const _conditionalTypes: any = {}
// 2. then replace ln:12303 to ln:12360
function trackConditionalType() {
// one time stuff
if (_conditionalTypes._callStack === undefined) _conditionalTypes._callStack = []
if (_conditionalTypes._results === undefined) _conditionalTypes._results = []
const top: any[] = []
_conditionalTypes._callStack.push(top)
}
function pushTop(index: number, elm: any) {
const top = _conditionalTypes._callStack[_conditionalTypes._callStack.length - 1]
if (_conditionalTypes._fetchMe) {
top[index] = _conditionalTypes._fetchMe
_conditionalTypes._fetchMe = null
} else {
top[index] = elm
}
return elm
}
function untrackConditionalType() {
const popped = _conditionalTypes._callStack.pop()
_conditionalTypes._fetchMe = popped
if (_conditionalTypes._callStack.length === 0) {
_conditionalTypes._results.push(_conditionalTypes._fetchMe)
_conditionalTypes._fetchMe = null
}
}
function getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
try {
trackConditionalType()
return _getConditionalType(root, mapper)
} finally {
untrackConditionalType()
}
}
function _getConditionalType(root: ConditionalRoot, mapper: TypeMapper | undefined): Type {
const checkType = instantiateType(root.checkType, mapper);
pushTop(0, checkType)
const extendsType = instantiateType(root.extendsType, mapper);
pushTop(1, extendsType)
if (checkType === wildcardType || extendsType === wildcardType) {
return wildcardType;
}
const checkTypeInstantiable = maybeTypeOfKind(checkType, TypeFlags.Instantiable | TypeFlags.GenericMappedType);
let combinedMapper: TypeMapper | undefined;
if (root.inferTypeParameters) {
const context = createInferenceContext(root.inferTypeParameters, /*signature*/ undefined, InferenceFlags.None);
if (!checkTypeInstantiable) {
// We don't want inferences from constraints as they may cause us to eagerly resolve the
// conditional type instead of deferring resolution. Also, we always want strict function
// types rules (i.e. proper contravariance) for inferences.
inferTypes(context.inferences, checkType, extendsType, InferencePriority.NoConstraints | InferencePriority.AlwaysStrict);
}
combinedMapper = combineTypeMappers(mapper, context.mapper);
}
// Instantiate the extends type including inferences for 'infer T' type parameters
const inferredExtendsType = combinedMapper ? instantiateType(root.extendsType, combinedMapper) : extendsType;
// We attempt to resolve the conditional type only when the check and extends types are non-generic
if (!checkTypeInstantiable && !maybeTypeOfKind(inferredExtendsType, TypeFlags.Instantiable | TypeFlags.GenericMappedType)) {
if (inferredExtendsType.flags & TypeFlags.AnyOrUnknown) {
return pushTop(2, instantiateType(root.trueType, combinedMapper || mapper));
}
// Return union of trueType and falseType for 'any' since it matches anything
if (checkType.flags & TypeFlags.Any) {
return pushTop(4, getUnionType([instantiateType(root.trueType, combinedMapper || mapper), instantiateType(root.falseType, mapper)]) );
}
// Return falseType for a definitely false extends check. We check an instantiations of the two
// types with type parameters mapped to the wildcard type, the most permissive instantiations
// possible (the wildcard type is assignable to and from all types). If those are not related,
// then no instantiations will be and we can just return the false branch type.
if (!isTypeAssignableTo(getPermissiveInstantiation(checkType), getPermissiveInstantiation(inferredExtendsType))) {
return pushTop(3, instantiateType(root.falseType, mapper));
}
// Return trueType for a definitely true extends check. We check instantiations of the two
// types with type parameters mapped to their restrictive form, i.e. a form of the type parameter
// that has no constraint. This ensures that, for example, the type
// type Foo<T extends { x: any }> = T extends { x: string } ? string : number
// doesn't immediately resolve to 'string' instead of being deferred.
if (isTypeAssignableTo(getRestrictiveInstantiation(checkType), getRestrictiveInstantiation(inferredExtendsType))) {
return pushTop(2, instantiateType(root.trueType, combinedMapper || mapper));
}
}
// Return a deferred type for a check that is neither definitely true nor definitely false
const erasedCheckType = getActualTypeVariable(checkType);
const result = <ConditionalType>createType(TypeFlags.Conditional);
result.root = root;
result.checkType = erasedCheckType;
result.extendsType = extendsType;
result.mapper = mapper;
result.combinedMapper = combinedMapper;
result.aliasSymbol = root.aliasSymbol;
result.aliasTypeArguments = instantiateTypes(root.aliasTypeArguments, mapper!); // TODO: GH#18217
pushTop(5, result);
return result;
}
type IfNumberThenBoolean<N> = N extends number ? boolean : string;
type IfNumberThenNull<X> = IfNumberThenBoolean<X> extends boolean
? null
: string;
type shouldBeNull = IfNumberThenNull<number>;
// change path to your local machine path:
const ts = require("/path/to/your/customized/typescript.js");
function getNodeForQuickInfo(node) {
if (ts.isNewExpression(node.parent) && node.pos === node.parent.pos) {
return node.parent.expression;
}
return node;
}
module.exports = function checkAgainst(filename, position, compilerOptions) {
const program = ts.createProgram([filename], compilerOptions);
const allSourceFiles = program.getSourceFiles();
const targetFile = allSourceFiles[allSourceFiles.length - 1];
const checker = program.getDiagnosticsProducingTypeChecker();
const node = ts.getTouchingPropertyName(targetFile, position);
const nodeForQuickInfo = getNodeForQuickInfo(node);
const typeOfNode = checker.getTypeAtLocation(nodeForQuickInfo);
const _results = ts._conditionalTypes._results;
// reset
ts._conditionalTypes = {};
return _results;
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment