in Clients/AmbrosiaJS/Ambrosia-Node/src/Meta.ts [1534:1704]
function checkType(context: TypeCheckContext, type: string, name: string | null, typeDescription: string, optionalAllowed: boolean = true, restPrefixAllowed: boolean = false): boolean
{
type = type.replace(/([ ]+)(?=[\[\]])/g, "").trim(); // Remove all space before (or between) array suffix characters ([])
// Look for a bad parameter (or object property) name, like "some-Name[]"
checkName(name, typeDescription, optionalAllowed, restPrefixAllowed);
if (type.length > 0)
{
// Since TS utility types are not meaningful without their generic-type specifier (which we always remove), we cannot support them
if (type.indexOf("<") > 0)
{
for (const tsUtilityType of _tsUtilityTypes)
{
if (type.startsWith(`${tsUtilityType}<`))
{
throw new Error(`The ${typeDescription} uses a TypeScript utility type ('${tsUtilityType}'); utility types are not supported`)
}
}
}
// For type-checking purposes we remove all TS generic specifiers, eg. "Map<number, string>" becomes "Map"; generics [currently] aren't part of native JS.
// But since the generic specifier(s) will still be used in TS wrappers, we need to check that the generic types are valid too; this also checks that the
// generic types are serializable.
for (const genericsSpecifier of Type.getGenericsSpecifiers(type))
{
for (const genericType of Type.parseGenericsSpecifier(genericsSpecifier))
{
checkType(context, genericType, name, `generic-type specifier in ${typeDescription}`, optionalAllowed, restPrefixAllowed);
}
}
type = Type.removeGenericsSpecifiers(type);
// Check against the built-in [or published] types, and arrays of built-in [or published] types
if (type[0] !== "{") // A named type, not a complex/compound type
{
// Note: Even though "any" is not a native JavaScript type, we (like TypeScript) use it as the "no specific type" type
let validTypeNames: string[] = Type.getSupportedNativeTypes(false).concat(Object.keys(_publishedTypes)).concat(["any"]);
let lcType: string = type.toLowerCase();
for (let i = 0; i < validTypeNames.length; i++)
{
if ((type === validTypeNames[i]) || type.startsWith(validTypeNames[i] + "[]")) // We want to include arrays of arrays, eg. string[][]
{
if (type.startsWith("any") || type.startsWith("object")) // Include any[] and object[]
{
Utils.log(`Warning: The ${typeDescription} uses type '${type}' which is too general to determine if it can be safely serialized; it is strongly recommended to use a more specific type`);
}
// We allow null and undefined, but only when being used in a union type (eg. "string | null")
// [Note that "string & null" is 'never', so we don't support null (or undefined) with intersection types]
if (((type === "null") || (type === "undefined")) && (typeDescription.indexOf("union") === -1)) // TODO: Checking typeDescription is brittle
{
throw new Error(`The ${typeDescription} has a type ('${type}') that's not supported in this context`);
}
// "null[]" and "undefined[]" are nonsensical types
if (type.startsWith("null[") || type.startsWith("undefined["))
{
throw new Error(`The ${typeDescription} has an unsupported type ('${type}')`);
}
return (true); // Success
}
// Check for mismatched casing [Note: We disallow published type names that differ only by case]
if (Utils.equalIgnoringCase(type, validTypeNames[i]) || lcType.startsWith(validTypeNames[i].toLowerCase() + "[]"))
{
let brackets: string = type.replace(/[^\[\]]+/g, "");
throw new Error(`The ${typeDescription} has an invalid type ('${type}'); did you mean '${validTypeNames[i] + brackets}'?`);
}
}
// We do this to get a better error message; without it the error message asks the user to publish the missing type, which is invalid for a native (or TS built-in) type.
for (const unsupportedTypeName of Type.getUnsupportedNativeTypes())
{
if (type === unsupportedTypeName)
{
throw new Error(`The ${typeDescription} has an unsupported type ('${type}')`);
}
}
// We support template string types (technically TemplateLiteralType) on a "best effort" basis, since the syntax can be arbitrarily complex.
// Our goal is to cover the common/basic use case for these types (ie. with unions of string literals).
// Note: Template string types will end up with an expanded definition of "string" (for simplicity).
if (type.indexOf("`") === 0) // Eg. "`Hello ${number}`"
{
const templateStringRegEx: RegExp = /(?<=\${)[^/}]+(?=})/g;// Find all the "type" in "${type}" templates
let result: RegExpExecArray | null;
let templateNumber: number = 1;
while (result = templateStringRegEx.exec(type)) // Because regex use /g, exec() does a stateful search, returning only the next match with each call
{
const templateType: string = result[0];
// Note: Since we're always going to use "string" as the expandedType, the only reason to check the individual template types is to catch
// errors when NOT doing code-gen from source [the TypeScript compiler will have already done the checking in that case], ie. when
// publishing "manually" using hand-written publishType() calls.
checkType(context, templateType, name, `replacement template #${templateNumber++} of ${typeDescription}`, optionalAllowed, restPrefixAllowed);
}
return (true);
}
// A string literal is valid (eg. as can appear in a union)
if (/^"[^"]*"$/.test(type) || /^'[^']*'$/.test(type))
{
return (true);
}
// We support union and intersection types on a "best effort" basis, since the syntax can be arbitrarily complex. Our goal is to cover all the common/basic use cases for these types.
// Note: Union and intersection types will end up with an expanded definition of "any", which is done to opt them out of runtime type checking (for simplicity). But we still want to
// check each type used in the the union/intersection so that we can verify they are serializable.
if ((type.indexOf("|") !== -1) || (type.indexOf("&") !== -1)) // Eg. "string | (number | boolean)[]" or "Foo & (Bar | Baz)[]"
{
const result: { kindFound: CompoundTypeKind, components: string[] } = Type.getCompoundTypeComponents(type);
if (result.components.length > 0)
{
const kindName: string = CompoundTypeKind[result.kindFound].toLowerCase();
result.components.map((t, i) => checkType(context, t, name, `${kindName}-type component #${i + 1} of ${typeDescription}`, optionalAllowed, restPrefixAllowed));
return (true);
}
}
// Check for unsupported types (which are unsupported either for simplicity of our code, or because it's not practical/feasible to support it)
if (type.indexOf("[") === 0) // Eg. "[string, number]"
{
// Note: The developer can downcast a tuple to an any[] to pass it to an Ambrosia method
throw new Error(`The ${typeDescription} has an invalid type ('${type}'); tuple types are not supported`);
}
if (type.indexOf("(") === 0) // Eg. "(p1: string) => number"
{
throw new Error(`The ${typeDescription} has an invalid type ('${type}'); function types are not supported`);
}
// Conditional types are almost always used with generics, whose use will have been caught by AST.publishTypeAlias() and AST.publishFunction().
// But there are still valid non-generic (if nonsensical) conditional types that will slip by those checks, and this check will still apply
// if publishing "manually" using hand-written publishType() calls.
if (type.indexOf(" extends ") !== -1) // Eg. "string extends undefined? never : string" (this is a valid, but nonsensical, example)
{
throw new Error(`The ${typeDescription} has an invalid type ('${type}'); conditional types are not supported`);
}
// The type is either an incorrectly spelled native type, or is a yet-to-be published custom type.
// We'll assume the latter, since this allows forward references to be used when publishing types.
let missingTypeName: string = type.replace(/\[]/g, ""); // Remove all square brackets (eg. "Name[]" => "Name")
// Since we're expecting the type to be a name, we check that it is indeed a valid name. This is to catch "unexpected" TypeScript syntax that's other than
// Tuple/Function type syntax, since we only check for these 2 cases above - but other cases exist (including in future versions of TypeScript).
try
{
checkName(missingTypeName, typeDescription, false);
}
catch
{
throw new Error(`The ${typeDescription} has an unsupported definition (${type})`);
}
if (context === TypeCheckContext.Type)
{
if (!_missingTypes.has(missingTypeName))
{
_missingTypes.set(missingTypeName, typeDescription);
}
}
// We don't allow forward references for a method parameter/return type, because we don't do any "deferred checking" for methods as we do for types [see updateUnexpandedPublishedTypes()]
if (context === TypeCheckContext.Method)
{
throw new Error(`The ${typeDescription} references an unpublished type ('${type}')`);
}
return (true);
}