UNPKG

ts-simple-type

Version:

Relationship type checker functions for Typescript types.

1,068 lines (1,052 loc) 46.2 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var tsModuleType = require('typescript'); (function (SimpleTypeKind) { SimpleTypeKind["STRING_LITERAL"] = "STRING_LITERAL"; SimpleTypeKind["NUMBER_LITERAL"] = "NUMBER_LITERAL"; SimpleTypeKind["BOOLEAN_LITERAL"] = "BOOLEAN_LITERAL"; SimpleTypeKind["BIG_INT_LITERAL"] = "BIG_INT_LITERAL"; SimpleTypeKind["STRING"] = "STRING"; SimpleTypeKind["NUMBER"] = "NUMBER"; SimpleTypeKind["BOOLEAN"] = "BOOLEAN"; SimpleTypeKind["BIG_INT"] = "BIG_INT"; SimpleTypeKind["NULL"] = "NULL"; SimpleTypeKind["UNDEFINED"] = "UNDEFINED"; SimpleTypeKind["ANY"] = "ANY"; SimpleTypeKind["UNKNOWN"] = "UNKNOWN"; SimpleTypeKind["VOID"] = "VOID"; SimpleTypeKind["UNION"] = "UNION"; SimpleTypeKind["ENUM"] = "ENUM"; SimpleTypeKind["ENUM_MEMBER"] = "ENUM_MEMBER"; SimpleTypeKind["INTERSECTION"] = "INTERSECTION"; SimpleTypeKind["TUPLE"] = "TUPLE"; SimpleTypeKind["INTERFACE"] = "INTERFACE"; SimpleTypeKind["OBJECT"] = "OBJECT"; SimpleTypeKind["FUNCTION"] = "FUNCTION"; SimpleTypeKind["METHOD"] = "METHOD"; SimpleTypeKind["CLASS"] = "CLASS"; SimpleTypeKind["CIRCULAR_TYPE_REF"] = "CIRCULAR_TYPE_REF"; SimpleTypeKind["GENERIC_ARGUMENTS"] = "GENERIC_ARGUMENTS"; SimpleTypeKind["GENERIC_PARAMETER"] = "GENERIC_PARAMETER"; SimpleTypeKind["ALIAS"] = "ALIAS"; SimpleTypeKind["DATE"] = "DATE"; SimpleTypeKind["ARRAY"] = "ARRAY"; SimpleTypeKind["PROMISE"] = "PROMISE"; })(exports.SimpleTypeKind || (exports.SimpleTypeKind = {})); (function (SimpleTypeModifierKind) { SimpleTypeModifierKind["EXPORT"] = "EXPORT"; SimpleTypeModifierKind["AMBIENT"] = "AMBIENT"; SimpleTypeModifierKind["PUBLIC"] = "PUBLIC"; SimpleTypeModifierKind["PRIVATE"] = "PRIVATE"; SimpleTypeModifierKind["PROTECTED"] = "PROTECTED"; SimpleTypeModifierKind["STATIC"] = "STATIC"; SimpleTypeModifierKind["READONLY"] = "READONLY"; SimpleTypeModifierKind["ABSTRACT"] = "ABSTRACT"; SimpleTypeModifierKind["ASYNC"] = "ASYNC"; SimpleTypeModifierKind["DEFAULT"] = "DEFAULT"; })(exports.SimpleTypeModifierKind || (exports.SimpleTypeModifierKind = {})); function isSimpleType(type) { return typeof type === "object" && "kind" in type && Object.keys(exports.SimpleTypeKind).find((key) => exports.SimpleTypeKind[key] === type.kind) != null; } const LITERAL_TYPE_KINDS = [exports.SimpleTypeKind.STRING_LITERAL, exports.SimpleTypeKind.NUMBER_LITERAL, exports.SimpleTypeKind.BOOLEAN_LITERAL, exports.SimpleTypeKind.BIG_INT_LITERAL]; function isSimpleTypeLiteral(type) { return LITERAL_TYPE_KINDS.includes(type.kind); } const PRIMITIVE_TYPE_KINDS = [ ...LITERAL_TYPE_KINDS, exports.SimpleTypeKind.STRING, exports.SimpleTypeKind.NUMBER, exports.SimpleTypeKind.BOOLEAN, exports.SimpleTypeKind.BIG_INT, exports.SimpleTypeKind.NULL, exports.SimpleTypeKind.UNDEFINED ]; function isSimpleTypePrimitive(type) { return PRIMITIVE_TYPE_KINDS.includes(type.kind); } const PRIMITIVE_TYPE_TO_LITERAL_MAP = { [exports.SimpleTypeKind.STRING]: exports.SimpleTypeKind.STRING_LITERAL, [exports.SimpleTypeKind.NUMBER]: exports.SimpleTypeKind.NUMBER_LITERAL, [exports.SimpleTypeKind.BOOLEAN]: exports.SimpleTypeKind.BOOLEAN_LITERAL, [exports.SimpleTypeKind.BIG_INT]: exports.SimpleTypeKind.BIG_INT_LITERAL }; const IMPLICIT_GENERIC = [exports.SimpleTypeKind.ARRAY, exports.SimpleTypeKind.TUPLE, exports.SimpleTypeKind.PROMISE]; function isImplicitGenericType(type) { return IMPLICIT_GENERIC.includes(type.kind); } const tsModule = { ts: tsModuleType }; function setTypescriptModule(newModule) { tsModule.ts = newModule; } function or(list, match) { return list.find((a, i) => match(a, i)) != null; } function and(list, match) { return list.find((a, i) => !match(a, i)) == null; } function isNode(obj) { return obj != null && typeof obj === "object" && "kind" in obj && "flags" in obj && "pos" in obj && "end" in obj; } function hasFlag(type, flag, op = "and") { if (Array.isArray(flag)) { return (op === "and" ? and : or)(flag, f => hasFlag(type, f)); } return (type.flags & flag) !== 0; } function isBoolean(type) { return hasFlag(type, tsModule.ts.TypeFlags.BooleanLike); } function isBooleanLiteral(type) { return hasFlag(type, tsModule.ts.TypeFlags.BooleanLiteral); } function isBigIntLiteral(type) { return hasFlag(type, tsModule.ts.TypeFlags.BigIntLiteral); } function isLiteral(type) { return type.isLiteral() || isBooleanLiteral(type) || isBigIntLiteral(type); } function isString(type) { return hasFlag(type, tsModule.ts.TypeFlags.StringLike); } function isNumber(type) { return hasFlag(type, tsModule.ts.TypeFlags.NumberLike); } function isEnum(type) { return hasFlag(type, tsModule.ts.TypeFlags.EnumLike); } function isBigInt(type) { return hasFlag(type, tsModule.ts.TypeFlags.BigIntLike); } function isObject(type) { return hasFlag(type, tsModule.ts.TypeFlags.Object); } function isUnknown(type) { return hasFlag(type, tsModule.ts.TypeFlags.Unknown); } function isNull(type) { return hasFlag(type, tsModule.ts.TypeFlags.Null); } function isUndefined(type) { return hasFlag(type, tsModule.ts.TypeFlags.Undefined); } function isVoid(type) { return hasFlag(type, tsModule.ts.TypeFlags.VoidLike); } function isObjectTypeReference(type) { return (type.objectFlags & tsModule.ts.ObjectFlags.Reference) !== 0; } function isMethod(type) { if (!isObject(type)) return false; const symbol = type.getSymbol(); if (symbol == null) return false; const decl = getDeclaration(symbol); if (decl == null) return false; return tsModule.ts.isMethodDeclaration(decl); } function getDeclaration(symbol) { // Note: Somehow "symbol.declarations" can be undefined under some circumstances if (symbol.declarations == null) return undefined; return symbol.declarations.length > 0 ? symbol.declarations[0] : symbol.valueDeclaration; } function isArray(type) { if (!isObject(type)) return false; const symbol = type.getSymbol(); if (symbol == null) return false; return getTypeArguments(type).length === 1 && ["ReadonlyArray", "Array"].includes(symbol.getName()); //return symbol.getName() === "Array"; // && getTypeArguments(type).length === 1; } function isPromise(type) { if (!isObject(type)) return false; const symbol = type.getSymbol(); if (symbol == null) return false; return getTypeArguments(type).length === 1 && symbol.getName() === "Promise"; } function isDate(type) { if (!isObject(type)) return false; const symbol = type.getSymbol(); if (symbol == null) return false; return symbol.getName() === "Date"; } function isTuple(type) { const target = getTargetType(type); if (target == null) return false; return (target.objectFlags & tsModule.ts.ObjectFlags.Tuple) !== 0; } function isFunction(type) { if (!isObject(type)) return false; const symbol = type.getSymbol(); if (symbol == null) return false; return (symbol.flags & tsModule.ts.SymbolFlags.Function) !== 0 || (symbol.members != null && symbol.members.has("__call")); } function getTypeArguments(type) { if (isObject(type)) { if (isObjectTypeReference(type)) { return Array.from(type.typeArguments || []); } } return []; } function getTargetType(type) { if (isObject(type) && isObjectTypeReference(type)) { return type.target; } } function getModifiersFromDeclaration(declaration) { const tsModifiers = tsModule.ts.getCombinedModifierFlags(declaration); const modifiers = []; const map = { [tsModule.ts.ModifierFlags.Export]: exports.SimpleTypeModifierKind.EXPORT, [tsModule.ts.ModifierFlags.Ambient]: exports.SimpleTypeModifierKind.AMBIENT, [tsModule.ts.ModifierFlags.Public]: exports.SimpleTypeModifierKind.PUBLIC, [tsModule.ts.ModifierFlags.Private]: exports.SimpleTypeModifierKind.PRIVATE, [tsModule.ts.ModifierFlags.Protected]: exports.SimpleTypeModifierKind.PROTECTED, [tsModule.ts.ModifierFlags.Static]: exports.SimpleTypeModifierKind.STATIC, [tsModule.ts.ModifierFlags.Readonly]: exports.SimpleTypeModifierKind.READONLY, [tsModule.ts.ModifierFlags.Abstract]: exports.SimpleTypeModifierKind.ABSTRACT, [tsModule.ts.ModifierFlags.Async]: exports.SimpleTypeModifierKind.ASYNC, [tsModule.ts.ModifierFlags.Default]: exports.SimpleTypeModifierKind.DEFAULT }; Object.entries(map).forEach(([tsModifier, modifierKind]) => { if ((tsModifiers & Number(tsModifier)) !== 0) { modifiers.push(modifierKind); } }); return modifiers; } function toSimpleType(type, checker, cache) { if (isNode(type)) { // "type" is a "Node", convert it to a "Type" and continue. return toSimpleType(checker.getTypeAtLocation(type), checker); } return toSimpleTypeInternalCaching(type, { checker, circularCache: new WeakMap(), cache: cache || new WeakMap() }); } function toSimpleTypeInternalCaching(type, options) { const placeholder = {}; // Cache the result of the conversion to a SimpleType if the type doesn't refer to an alias and is not a type parameter. // Here we make sure that all SimpleTypes refer to the same instance. if (!options.cache.has(type)) { if (type.aliasSymbol == null && type.aliasTypeArguments == null && !type.isTypeParameter() && !(isObject(type) && isObjectTypeReference(type))) { options.cache.set(type, placeholder); } } else { return options.cache.get(type); } if (options.circularCache.has(type)) { return { kind: exports.SimpleTypeKind.CIRCULAR_TYPE_REF, ref: options.circularCache.get(type) }; } else { // Connect the type to the placeholder reference // Circular types will point to this reference // Only return a circular ref if it's a class of interface. // Don't return circular ref if it's a primitive like a "number" if (type.isClassOrInterface() || (isObject(type) && isObjectTypeReference(type))) { options.circularCache.set(type, placeholder); } } // Construct the simple type recursively const simpleTypeOverwrite = toSimpleTypeInternal(type, options); // Strip undefined keys to make the output cleaner Object.entries(simpleTypeOverwrite).forEach(([k, v]) => { if (v == null) delete simpleTypeOverwrite[k]; }); // Transfer properties on the simpleType to the placeholder // This makes it possible to keep on using the reference "placeholder". Object.assign(placeholder, simpleTypeOverwrite); // Try to lift a potential generic type and wrap the result in a "GENERIC_ARGUMENTS" simple type and/or "ALIAS" type. return liftGenericType(placeholder, type, options); } /** * Tries to lift a potential generic type and wrap the result in a "GENERIC_ARGUMENTS" simple type and/or "ALIAS" type. * Returns the "simpleType" otherwise. * @param simpleType * @param type * @param options */ function liftGenericType(simpleType, type, options) { // Check for alias reference if (type.aliasSymbol != null) { const aliasDeclaration = getDeclaration(type.aliasSymbol); const typeParameters = getTypeParameters(aliasDeclaration, options); // Lift the simple type to an ALIAS type. const aliasType = { kind: exports.SimpleTypeKind.ALIAS, name: type.aliasSymbol.getName(), target: simpleType, typeParameters }; // Lift the alias type if it uses generic arguments. if (type.aliasTypeArguments != null) { const typeArguments = Array.from(type.aliasTypeArguments || []).map(t => toSimpleTypeInternalCaching(t, options)); return { kind: exports.SimpleTypeKind.GENERIC_ARGUMENTS, target: aliasType, typeArguments }; } return aliasType; } // Check if the type is a generic interface/class reference and lift it. else if (isObject(type) && isObjectTypeReference(type) && type.typeArguments != null) { // Special case for array, tuple and promise, they are generic in themselves if (isImplicitGenericType(simpleType)) { return simpleType; } const typeArguments = Array.from(type.typeArguments || []).map(t => toSimpleTypeInternalCaching(t, options)); return { kind: exports.SimpleTypeKind.GENERIC_ARGUMENTS, target: simpleType, typeArguments }; } return simpleType; } function toSimpleTypeInternal(type, options) { const { checker } = options; const symbol = type.getSymbol(); const name = symbol != null ? getRealSymbolName(symbol) : undefined; if (isLiteral(type)) { const literalSimpleType = literalToSimpleType(type, checker); if (literalSimpleType != null) { // Enum members if (symbol != null && symbol.flags & tsModule.ts.SymbolFlags.EnumMember) { const parentSymbol = symbol.parent; if (parentSymbol != null) { return { name: name || "", fullName: `${parentSymbol.name}.${name}`, kind: exports.SimpleTypeKind.ENUM_MEMBER, type: literalSimpleType }; } } // Literals types return literalSimpleType; } } // Primitive types else if (isString(type)) { return { kind: exports.SimpleTypeKind.STRING, name }; } else if (isNumber(type)) { return { kind: exports.SimpleTypeKind.NUMBER, name }; } else if (isBoolean(type)) { return { kind: exports.SimpleTypeKind.BOOLEAN, name }; } else if (isBigInt(type)) { return { kind: exports.SimpleTypeKind.BIG_INT, name }; } else if (isUndefined(type)) { return { kind: exports.SimpleTypeKind.UNDEFINED, name }; } else if (isNull(type)) { return { kind: exports.SimpleTypeKind.NULL, name }; } else if (isUnknown(type)) { return { kind: exports.SimpleTypeKind.UNKNOWN, name }; } else if (isVoid(type)) { return { kind: exports.SimpleTypeKind.VOID, name }; } // Enum else if (isEnum(type) && type.isUnion()) { return { name: name || "", kind: exports.SimpleTypeKind.ENUM, types: type.types.map(t => toSimpleTypeInternalCaching(t, options)) }; } // Promise else if (isPromise(type)) { return { kind: exports.SimpleTypeKind.PROMISE, name, type: toSimpleTypeInternalCaching(getTypeArguments(type)[0], options) }; } // Unions and intersections else if (type.isUnion()) { return { kind: exports.SimpleTypeKind.UNION, types: simplifySimpleTypeArray(type.types.map(t => toSimpleTypeInternalCaching(t, options))), name }; } else if (type.isIntersection()) { return { kind: exports.SimpleTypeKind.INTERSECTION, types: simplifySimpleTypeArray(type.types.map(t => toSimpleTypeInternalCaching(t, options))), name }; } // Date else if (isDate(type)) { return { kind: exports.SimpleTypeKind.DATE, name }; } // Array else if (isArray(type)) { return { kind: exports.SimpleTypeKind.ARRAY, type: toSimpleTypeInternalCaching(getTypeArguments(type)[0], options), name }; } else if (isTuple(type)) { const types = getTypeArguments(type); return { kind: exports.SimpleTypeKind.TUPLE, members: types.map(childType => { const childSymbol = childType.getSymbol(); return { optional: childSymbol != null ? (childSymbol.flags & tsModule.ts.SymbolFlags.Optional) !== 0 : false, type: toSimpleTypeInternalCaching(childType, options) }; }), name }; } // Function else if (symbol != null && (isFunction(type) || isMethod(type))) { const functionDeclaration = getDeclaration(symbol); if (functionDeclaration != null) { const simpleType = getSimpleFunctionFromDeclaration(functionDeclaration, options, true); if (simpleType != null) { simpleType.name = simpleType.name || name; } if (simpleType != null) { return simpleType; } } } // Alternative way of getting functions else if (isObject(type) && type.getCallSignatures().length > 0) { const functionDeclaration = type.getCallSignatures()[0].getDeclaration(); const simpleType = getSimpleFunctionFromDeclaration(functionDeclaration, options, true); if (simpleType != null) { return simpleType; } } // Class else if (type.isClass() && symbol != null) { const classDecl = getDeclaration(symbol); if (classDecl != null && tsModule.ts.isClassDeclaration(classDecl)) { const ctor = (() => { const ctorSymbol = symbol != null && symbol.members != null ? symbol.members.get("__constructor") : undefined; if (ctorSymbol != null && symbol != null) { const ctorDecl = ctorSymbol.declarations.length > 0 ? ctorSymbol.declarations[0] : ctorSymbol.valueDeclaration; if (ctorDecl != null && tsModule.ts.isConstructorDeclaration(ctorDecl)) { return getSimpleFunctionFromDeclaration(ctorDecl, options, false); } } })(); const members = checker.getPropertiesOfType(type).map(symbol => ({ name: symbol.name, modifiers: getModifiersFromDeclaration(symbol.valueDeclaration), type: toSimpleTypeInternalCaching(checker.getTypeAtLocation(symbol.valueDeclaration), options) })); const typeParameters = getTypeParameters(getDeclaration(symbol), options); return { kind: exports.SimpleTypeKind.CLASS, name, ctor, typeParameters, properties: members.filter(m => m.type.kind !== exports.SimpleTypeKind.METHOD), methods: members.filter(m => m.type.kind === exports.SimpleTypeKind.METHOD) }; } } // Interface else if (type.isClassOrInterface() || isObject(type)) { const members = checker.getPropertiesOfType(type).map(symbol => ({ name: symbol.name, optional: (symbol.flags & tsModule.ts.SymbolFlags.Optional) !== 0, type: toSimpleTypeInternalCaching(checker.getTypeAtLocation(symbol.valueDeclaration), options) })); const typeParameters = (isObjectTypeReference(type) && type.target.typeParameters ? type.target.typeParameters.map(t => toSimpleTypeInternalCaching(t, options)) : undefined) || (symbol != null ? getTypeParameters(getDeclaration(symbol), options) : undefined); return { kind: type.isClassOrInterface() ? exports.SimpleTypeKind.INTERFACE : exports.SimpleTypeKind.OBJECT, typeParameters, members, name }; } if (type.isTypeParameter() && symbol != null) { const defaultType = type.getDefault(); const defaultSimpleType = defaultType != null ? toSimpleTypeInternalCaching(defaultType, options) : undefined; return { kind: exports.SimpleTypeKind.GENERIC_PARAMETER, name: symbol.getName(), default: defaultSimpleType }; } return { kind: exports.SimpleTypeKind.ANY, name }; } function simplifySimpleTypeArray(types) { let newTypes = [...types]; const NULLABLE_TYPE_KINDS = [exports.SimpleTypeKind.UNDEFINED, exports.SimpleTypeKind.NULL]; // Only include one instance of primitives newTypes = newTypes.filter((type, i) => { if (isSimpleTypeLiteral(type)) return true; if (PRIMITIVE_TYPE_KINDS.includes(type.kind) || NULLABLE_TYPE_KINDS.includes(type.kind)) { // Remove this type from the array if there is already a primitive in the array return !newTypes .slice(0, i) .map(t => t.kind) .includes(type.kind); } return true; }); // Simplify boolean literals const booleanLiteralTypes = newTypes.filter((t) => t.kind === exports.SimpleTypeKind.BOOLEAN_LITERAL); if (booleanLiteralTypes.find(t => t.value === true) != null && booleanLiteralTypes.find(t => t.value === false) != null) { newTypes = [...newTypes.filter(type => type.kind !== exports.SimpleTypeKind.BOOLEAN_LITERAL), { kind: exports.SimpleTypeKind.BOOLEAN }]; } // Reorder "NULL" and "UNDEFINED" to be last const nullableTypes = newTypes.filter((t) => NULLABLE_TYPE_KINDS.includes(t.kind)); if (nullableTypes.length > 0) { newTypes = [ ...newTypes.filter(t => !NULLABLE_TYPE_KINDS.includes(t.kind)), ...nullableTypes.sort((t1, t2) => (t1.kind === exports.SimpleTypeKind.NULL ? (t2.kind === exports.SimpleTypeKind.UNDEFINED ? -1 : 0) : t2.kind === exports.SimpleTypeKind.NULL ? 1 : 0)) ]; } return newTypes; } function literalToSimpleType(type, checker) { if (type.isNumberLiteral()) { return { kind: exports.SimpleTypeKind.NUMBER_LITERAL, value: type.value }; } else if (type.isStringLiteral()) { return { kind: exports.SimpleTypeKind.STRING_LITERAL, value: type.value }; } else if (isBooleanLiteral(type)) { // See https://github.com/Microsoft/TypeScript/issues/22269 for more information return { kind: exports.SimpleTypeKind.BOOLEAN_LITERAL, value: checker.typeToString(type) === "true" }; } else if (isBigIntLiteral(type)) { return { kind: exports.SimpleTypeKind.BIG_INT_LITERAL, value: BigInt(`${type.value.negative ? "-" : ""}${type.value.base10Value}`) }; } } function getSimpleFunctionFromDeclaration(functionDeclaration, options, checkReturnType) { const { checker } = options; const symbol = checker.getSymbolAtLocation(functionDeclaration); const type = checker.getTypeAtLocation(functionDeclaration); if (tsModule.ts.isFunctionLike(functionDeclaration)) { const signature = checker.getSignatureFromDeclaration(functionDeclaration); if (signature != null) { const argTypes = functionDeclaration.parameters.map(parameterDecl => { const argType = checker.getTypeAtLocation(parameterDecl); return { name: parameterDecl.name.getText(), optional: parameterDecl.questionToken != null, type: toSimpleTypeInternalCaching(argType, options), spread: parameterDecl.dotDotDotToken != null, initializer: parameterDecl.initializer != null }; }); const name = symbol != null ? symbol.getName() : undefined; const kind = isMethod(type) ? exports.SimpleTypeKind.METHOD : exports.SimpleTypeKind.FUNCTION; const returnType = toSimpleTypeInternalCaching(checker.getReturnTypeOfSignature(signature), options); const typeParameters = getTypeParameters(functionDeclaration, options); return { name, kind, returnType, argTypes, typeParameters }; } } } const BLACKLISTED_SYMBOL_NAMES = ["__type", "__object", "__function"]; function getRealSymbolName(symbol) { const name = symbol.getName(); if (name != null && BLACKLISTED_SYMBOL_NAMES.includes(name)) { return undefined; } return name; } function getTypeParameters(declaration, options) { if (declaration == null) return undefined; if (tsModule.ts.isClassDeclaration(declaration) || tsModule.ts.isFunctionDeclaration(declaration) || tsModule.ts.isTypeAliasDeclaration(declaration) || tsModule.ts.isMethodDeclaration(declaration)) { return declaration.typeParameters == null ? undefined : Array.from(declaration.typeParameters) .map(td => options.checker.getTypeAtLocation(td)) .map(t => toSimpleTypeInternalCaching(t, options)); } return undefined; } function isAssignableToSimpleTypeKind(type, kind, optionsOrChecker, options = {}) { if (!isSimpleType(type)) { return isAssignableToSimpleTypeKind(toSimpleType(type, optionsOrChecker), kind, options); } else { options = optionsOrChecker || {}; } // Make sure that an object without members is treated as ANY switch (type.kind) { case exports.SimpleTypeKind.OBJECT: if (type.members == null || type.members.length === 0) { return isAssignableToSimpleTypeKind({ kind: exports.SimpleTypeKind.ANY }, kind, options); } break; } switch (type.kind) { case exports.SimpleTypeKind.ENUM: case exports.SimpleTypeKind.UNION: return or(type.types, childType => isAssignableToSimpleTypeKind(childType, kind, options)); case exports.SimpleTypeKind.INTERSECTION: return and(type.types, childType => isAssignableToSimpleTypeKind(childType, kind, options)); case exports.SimpleTypeKind.ANY: return options.matchAny || false; case exports.SimpleTypeKind.ENUM_MEMBER: return isAssignableToSimpleTypeKind(type.type, kind, options); case exports.SimpleTypeKind.ALIAS: return isAssignableToSimpleTypeKind(type.target, kind, options); case exports.SimpleTypeKind.GENERIC_PARAMETER: return isAssignableToSimpleTypeKind(type.default || { kind: exports.SimpleTypeKind.ANY }, kind, options); default: if (Array.isArray(kind)) { return (options.op === "or" ? or : and)(kind, itemKind => type.kind === itemKind); } return type.kind === kind; } } function isAssignableToPrimitiveType(type, checker) { if (isSimpleType(type)) { return isAssignableToSimpleTypeKind(type, PRIMITIVE_TYPE_KINDS, { op: "or", matchAny: true }); } return isAssignableToSimpleTypeKind(type, PRIMITIVE_TYPE_KINDS, checker, { op: "or", matchAny: true }); } /** * Returns if typeB is assignable to typeA. * @param typeA Type A * @param typeB Type B */ function isAssignableToSimpleType(typeA, typeB) { return isAssignabletoSimpleTypeInternal(typeA, typeB, { inCircularA: false, inCircularB: false, insideType: new Set(), genericParameterMapA: new Map(), genericParameterMapB: new Map() }); } function isAssignabletoSimpleTypeInternal(typeA, typeB, options) { /* options = {...options}; (options as any).depth = ((options as any).depth || 0) + 1; console.log( "###", " ".repeat((options as any).depth), require("./simple-type-to-string").simpleTypeToString(typeA), "===", require("./simple-type-to-string").simpleTypeToString(typeB), "(", typeA.kind, "===", typeB.kind, ")", (options as any).depth, "###" ); */ if (typeA === typeB) { return true; } // We might need a better way of handling refs, but these check are good for now if (options.insideType.has(typeA)) { return true; } if (options.inCircularA && options.inCircularB) { return true; } if (typeA.kind === exports.SimpleTypeKind.UNKNOWN || typeA.kind === exports.SimpleTypeKind.ANY || typeB.kind === exports.SimpleTypeKind.ANY) { return true; } switch (typeB.kind) { case exports.SimpleTypeKind.CIRCULAR_TYPE_REF: return isAssignabletoSimpleTypeInternal(typeA, typeB.ref, { ...options, inCircularB: true, insideType: new Set([...options.insideType, typeB]) }); case exports.SimpleTypeKind.ENUM_MEMBER: return isAssignabletoSimpleTypeInternal(typeA, typeB.type, options); case exports.SimpleTypeKind.ENUM: return and(typeB.types, childTypeB => isAssignabletoSimpleTypeInternal(typeA, childTypeB, options)); case exports.SimpleTypeKind.UNION: return and(typeB.types, childTypeB => isAssignabletoSimpleTypeInternal(typeA, childTypeB, options)); case exports.SimpleTypeKind.INTERSECTION: return and(typeB.types, childTypeB => isAssignabletoSimpleTypeInternal(typeA, childTypeB, options)); case exports.SimpleTypeKind.ALIAS: return isAssignabletoSimpleTypeInternal(typeA, typeB.target, options); case exports.SimpleTypeKind.GENERIC_ARGUMENTS: return isAssignabletoSimpleTypeInternal(typeA, typeB.target, { ...options, genericParameterMapB: extendTypeParameterMap(typeB, options.genericParameterMapB) }); case exports.SimpleTypeKind.GENERIC_PARAMETER: const realType = options.genericParameterMapB.get(typeB.name); return isAssignabletoSimpleTypeInternal(typeA, realType || typeB.default || { kind: exports.SimpleTypeKind.ANY }, options); } switch (typeA.kind) { // Circular references case exports.SimpleTypeKind.CIRCULAR_TYPE_REF: return isAssignabletoSimpleTypeInternal(typeA.ref, typeB, { ...options, inCircularA: true, insideType: new Set([...options.insideType, typeA]) }); // Literals and enum members case exports.SimpleTypeKind.NUMBER_LITERAL: case exports.SimpleTypeKind.STRING_LITERAL: case exports.SimpleTypeKind.BIG_INT_LITERAL: case exports.SimpleTypeKind.BOOLEAN_LITERAL: return isSimpleTypeLiteral(typeB) ? typeA.value === typeB.value : false; case exports.SimpleTypeKind.ENUM_MEMBER: return isAssignabletoSimpleTypeInternal(typeA.type, typeB, options); // Primitive types case exports.SimpleTypeKind.STRING: case exports.SimpleTypeKind.BOOLEAN: case exports.SimpleTypeKind.NUMBER: case exports.SimpleTypeKind.BIG_INT: case exports.SimpleTypeKind.UNDEFINED: case exports.SimpleTypeKind.NULL: if (isSimpleTypeLiteral(typeB)) { return PRIMITIVE_TYPE_TO_LITERAL_MAP[typeA.kind] === typeB.kind; } return typeA.kind === typeB.kind; // Void case exports.SimpleTypeKind.VOID: return typeB.kind === exports.SimpleTypeKind.VOID; // Alias case exports.SimpleTypeKind.ALIAS: return isAssignabletoSimpleTypeInternal(typeA.target, typeB, options); // Generic types case exports.SimpleTypeKind.GENERIC_PARAMETER: const realType = options.genericParameterMapA.get(typeA.name); return isAssignabletoSimpleTypeInternal(realType || typeA.default || { kind: exports.SimpleTypeKind.ANY }, typeB, options); case exports.SimpleTypeKind.GENERIC_ARGUMENTS: return isAssignabletoSimpleTypeInternal(typeA.target, typeB, { ...options, genericParameterMapA: extendTypeParameterMap(typeA, options.genericParameterMapA) }); // Arrays case exports.SimpleTypeKind.ARRAY: if (typeB.kind === exports.SimpleTypeKind.ARRAY) { if (typeA) { return isAssignabletoSimpleTypeInternal(typeA.type, typeB.type, options); } } return false; // Functions case exports.SimpleTypeKind.FUNCTION: case exports.SimpleTypeKind.METHOD: if (typeB.kind !== exports.SimpleTypeKind.FUNCTION && typeB.kind !== exports.SimpleTypeKind.METHOD) return false; if (!isAssignabletoSimpleTypeInternal(typeA.returnType, typeB.returnType, options)) return false; for (let i = 0; i < Math.max(typeA.argTypes.length, typeB.argTypes.length); i++) { const argA = typeA.argTypes[i]; const argB = typeB.argTypes[i]; if (argB == null && argA != null && !argA.optional) { return false; } if (argB != null && argA == null) { return false; } if (!isAssignabletoSimpleTypeInternal(argA.type, argB.type, options)) { if (argA.spread && argA.type.kind === exports.SimpleTypeKind.ARRAY && (!argB.spread && argB.type.kind !== exports.SimpleTypeKind.ARRAY)) { if (!isAssignabletoSimpleTypeInternal(argA.type.type, argB.type, options)) { return false; } } } } return true; // Unions and enum members case exports.SimpleTypeKind.ENUM: case exports.SimpleTypeKind.UNION: return or(typeA.types, childTypeA => isAssignabletoSimpleTypeInternal(childTypeA, typeB, options)); // Intersections case exports.SimpleTypeKind.INTERSECTION: return and(typeA.types, childTypeA => isAssignabletoSimpleTypeInternal(childTypeA, typeB, options)); // Interfaces case exports.SimpleTypeKind.INTERFACE: case exports.SimpleTypeKind.OBJECT: case exports.SimpleTypeKind.CLASS: // If there are no members check that "typeB" is not assignable to 'null' and 'undefined'. // Here we allow assigning anything but 'null' and 'undefined' to the type '{}' if ("members" in typeA && (typeA.members == null || typeA.members.length === 0)) { return !isAssignabletoSimpleTypeInternal({ kind: exports.SimpleTypeKind.UNION, types: [{ kind: exports.SimpleTypeKind.NULL }, { kind: exports.SimpleTypeKind.UNDEFINED }] }, typeB, options); } switch (typeB.kind) { case exports.SimpleTypeKind.INTERFACE: case exports.SimpleTypeKind.OBJECT: case exports.SimpleTypeKind.CLASS: const membersA = typeA.kind === exports.SimpleTypeKind.CLASS ? [...typeA.methods, ...typeA.properties] : typeA.members || []; const membersB = typeB.kind === exports.SimpleTypeKind.CLASS ? [...typeB.methods, ...typeB.properties] : typeB.members || []; const newOptions = { ...options, insideType: new Set([...options.insideType, typeA, typeB]) }; return (and(membersA, memberA => { // Make sure that every required prop in typeA is present const memberB = membersB.find(memberB => memberA.name === memberB.name); return memberB == null ? memberA.optional : true; }) && and(membersB, memberB => { // Do not allow new props in subtype: contravariance // Strict type checking const memberA = membersA.find(memberA => memberA.name === memberB.name); if (memberA == null) return false; return isAssignabletoSimpleTypeInternal(memberA.type, memberB.type, newOptions); })); default: return false; } case exports.SimpleTypeKind.TUPLE: if (typeB.kind !== exports.SimpleTypeKind.TUPLE) return false; return and(typeA.members, (memberA, i) => { const memberB = typeB.members[i]; if (memberB == null) return memberA.optional; return isAssignabletoSimpleTypeInternal(memberA.type, memberB.type, options); }); case exports.SimpleTypeKind.PROMISE: return typeB.kind === exports.SimpleTypeKind.PROMISE && isAssignabletoSimpleTypeInternal(typeA.type, typeB.type, options); case exports.SimpleTypeKind.DATE: return typeB.kind === exports.SimpleTypeKind.DATE; //default: //throw new Error(`Unsupported comparison: ${typeA.kind}`); } } function extendTypeParameterMap(genericType, existingMap) { if ("typeParameters" in genericType.target) { const parameterEntries = (genericType.target.typeParameters || []).map((parameter, i) => [parameter.name, genericType.typeArguments[i] || parameter.default || { kind: exports.SimpleTypeKind.ANY }]); const allParameterEntries = [...existingMap.entries(), ...parameterEntries]; return new Map(allParameterEntries); } throw new Error(`Couldn't find 'typeParameter' for type '${genericType.target.kind}'`); //return existingMap; } const simpleTypeCache = new WeakMap(); const isAssignableTypeCache = new WeakMap(); function isAssignableToType(typeA, typeB, checker) { if (isNode(typeA)) { return isAssignableToType(checker.getTypeAtLocation(typeA), typeB, checker); } if (isNode(typeB)) { return isAssignableToType(typeA, checker.getTypeAtLocation(typeB), checker); } const simpleTypeA = isSimpleType(typeA) ? typeA : toSimpleType(typeA, checker, simpleTypeCache); const simpleTypeB = isSimpleType(typeB) ? typeB : toSimpleType(typeB, checker, simpleTypeCache); const typeAResultCache = (() => { if (isAssignableTypeCache.has(simpleTypeA)) { return isAssignableTypeCache.get(simpleTypeA); } const newResultCache = new WeakMap(); isAssignableTypeCache.set(simpleTypeA, newResultCache); return newResultCache; })(); if (typeAResultCache.has(simpleTypeB)) { return typeAResultCache.get(simpleTypeB); } /*console.log("Type A"); console.dir(simpleTypeA, { depth: 5 }); console.log("Type B"); console.dir(simpleTypeB, { depth: 5 });*/ const result = isAssignableToSimpleType(simpleTypeA, simpleTypeB); typeAResultCache.set(simpleTypeB, result); return result; } function isAssignableToValue(type, value, checker) { if (isSimpleType(type)) { if (typeof value === "string") { return isAssignableToType(type, { kind: exports.SimpleTypeKind.STRING_LITERAL, value }); } else if (typeof value === "number") { return isAssignableToType(type, { kind: exports.SimpleTypeKind.NUMBER_LITERAL, value }); } else if (typeof value === "boolean") { return isAssignableToType(type, { kind: exports.SimpleTypeKind.BOOLEAN_LITERAL, value }); } else if (value instanceof Promise) { return isAssignableToType(type, { kind: exports.SimpleTypeKind.PROMISE, type: { kind: exports.SimpleTypeKind.ANY } }); } else if (value instanceof Date) { return isAssignableToType(type, { kind: exports.SimpleTypeKind.DATE }); } throw new Error(`Comparing type "${type.kind}" to value ${value}, type ${typeof value} not supported yet.`); } return isAssignableToValue(toSimpleType(type, checker), value); } /** * Converts a simple type to a string. * @param type Simple Type */ function simpleTypeToString(type) { //}, options: SimpleTypeToStringOptions): string { switch (type.kind) { case exports.SimpleTypeKind.CIRCULAR_TYPE_REF: return type.ref.name || "[Circular]"; case exports.SimpleTypeKind.BOOLEAN_LITERAL: return String(type.value); case exports.SimpleTypeKind.NUMBER_LITERAL: return String(type.value); case exports.SimpleTypeKind.STRING_LITERAL: return `"${type.value}"`; case exports.SimpleTypeKind.BIG_INT_LITERAL: return `${type.value}n`; case exports.SimpleTypeKind.STRING: return "string"; case exports.SimpleTypeKind.BOOLEAN: return "boolean"; case exports.SimpleTypeKind.NUMBER: return "number"; case exports.SimpleTypeKind.BIG_INT: return "bigint"; case exports.SimpleTypeKind.UNDEFINED: return "undefined"; case exports.SimpleTypeKind.NULL: return "null"; case exports.SimpleTypeKind.ANY: return "any"; case exports.SimpleTypeKind.UNKNOWN: return "unknown"; case exports.SimpleTypeKind.VOID: return "void"; case exports.SimpleTypeKind.FUNCTION: case exports.SimpleTypeKind.METHOD: if (type.kind === exports.SimpleTypeKind.FUNCTION && type.name != null) return type.name; const argText = functionArgTypesToString(type.argTypes); return `(${argText})${type.returnType != null ? ` => ${simpleTypeToString(type.returnType)}` : ""}`; case exports.SimpleTypeKind.ARRAY: const hasMultipleTypes = [exports.SimpleTypeKind.UNION, exports.SimpleTypeKind.INTERSECTION].includes(type.type.kind); let memberType = simpleTypeToString(type.type); if (type.name != null && ["ReadonlyArray"].includes(type.name)) return `ReadonlyArray<${memberType}>`; if (hasMultipleTypes && type.type.name == null) memberType = `(${memberType})`; return `${memberType}[]`; case exports.SimpleTypeKind.UNION: if (type.name != null) return type.name; return type.types.map(simpleTypeToString).join(" | "); case exports.SimpleTypeKind.ENUM: return type.name; case exports.SimpleTypeKind.ENUM_MEMBER: return type.fullName; case exports.SimpleTypeKind.INTERSECTION: if (type.name != null) return type.name; return type.types.map(simpleTypeToString).join(" & "); case exports.SimpleTypeKind.INTERFACE: if (type.name != null) return type.name; // this fallthrough is intentional case exports.SimpleTypeKind.OBJECT: if (type.members == null || type.members.length === 0) return "{}"; return `{ ${type.members .map(member => { // this check needs to change in the future if (member.type.kind === exports.SimpleTypeKind.FUNCTION || member.type.kind === exports.SimpleTypeKind.METHOD) { const result = simpleTypeToString(member.type); const genericParameters = member.type.typeParameters != null ? `<${member.type.typeParameters.map(simpleTypeToString).join(", ")}>` : ""; return `${member.name}${result.replace(" => ", ": ").replace("(", `${genericParameters}(`)}`; } return `${member.name}: ${simpleTypeToString(member.type)}`; }) .join("; ")}${type.members.length > 0 ? ";" : ""} }`; case exports.SimpleTypeKind.TUPLE: return `[${type.members.map(member => `${simpleTypeToString(member.type)}${member.optional ? "?" : ""}`).join(", ")}]`; case exports.SimpleTypeKind.GENERIC_ARGUMENTS: const { target, typeArguments } = type; return typeArguments.length === 0 ? target.name || "" : `${target.name}<${typeArguments.map(simpleTypeToString).join(", ")}>`; case exports.SimpleTypeKind.PROMISE: return `${type.name || "Promise"}<${simpleTypeToString(type.type)}>`; case exports.SimpleTypeKind.DATE: return "Date"; default: return type.name || ""; } } function functionArgTypesToString(argTypes) { return argTypes .map(arg => { return `${arg.spread ? "..." : ""}${arg.name}${arg.optional ? "?" : ""}: ${simpleTypeToString(arg.type)}`; }) .join(", "); } function toTypeString(type, checker) { if (isSimpleType(type)) { return simpleTypeToString(type); } // Use the typescript checker to return a string for a type return checker.typeToString(type); } exports.setTypescriptModule = setTypescriptModule; exports.isAssignableToPrimitiveType = isAssignableToPrimitiveType; exports.isAssignableToType = isAssignableToType; exports.isAssignableToValue = isAssignableToValue; exports.toSimpleType = toSimpleType; exports.toTypeString = toTypeString; exports.isAssignableToSimpleTypeKind = isAssignableToSimpleTypeKind; exports.simpleTypeToString = simpleTypeToString; exports.isSimpleType = isSimpleType; exports.LITERAL_TYPE_KINDS = LITERAL_TYPE_KINDS; exports.isSimpleTypeLiteral = isSimpleTypeLiteral; exports.PRIMITIVE_TYPE_KINDS = PRIMITIVE_TYPE_KINDS; exports.isSimpleTypePrimitive = isSimpleTypePrimitive; exports.PRIMITIVE_TYPE_TO_LITERAL_MAP = PRIMITIVE_TYPE_TO_LITERAL_MAP; exports.IMPLICIT_GENERIC = IMPLICIT_GENERIC; exports.isImplicitGenericType = isImplicitGenericType;