CodeQL library for TypeScript¶
When you’re analyzing a TypeScript program, you can make use of the large collection of classes in the CodeQL library for TypeScript.
Overview¶
Support for analyzing TypeScript code is bundled with the CodeQL libraries for JavaScript, so you can include the full TypeScript library by importing the javascript.qll
module:
import javascript
CodeQL libraries for JavaScript covers most of this library, and is also relevant for TypeScript analysis. This document supplements the JavaScript documentation with the TypeScript-specific classes and predicates.
Syntax¶
Most syntax in TypeScript is represented in the same way as its JavaScript counterpart. For example, a+b
is represented by an AddExpr; the same as it would be in JavaScript. On the other hand, x as number
is represented by TypeAssertion, a class that is specific to TypeScript.
Type annotations¶
The TypeExpr class represents anything that is part of a type annotation.
Only type annotations that are explicit in the source code occur as a TypeExpr
. Types inferred by the TypeScript compiler are Type
entities; for details about this, see the section on static type information.
There are several ways to access type annotations, for example:
VariableDeclaration.getTypeAnnotation()
Function.getReturnTypeAnnotation()
BindingPattern.getTypeAnnotation()
Parameter.getTypeAnnotation()
(special case ofBindingPattern.getTypeAnnotation()
)VarDecl.getTypeAnnotation()
(special case ofBindingPattern.getTypeAnnotation()
)FieldDeclaration.getTypeAnnotation()
The TypeExpr class provides some convenient member predicates such as isString()
and isVoid()
to recognize commonly used types.
The subclasses that represent type annotations are:
- TypeAccess: a name referring to a type, such as
Date
orhttp.ServerRequest
.- LocalTypeAccess: an unqualified name, such as
Date
. - QualifiedTypeAccess: a name prefixed by a namespace, such as
http.ServerRequest
. - ImportTypeAccess: an
import
used as a type, such asimport("./foo")
.
- LocalTypeAccess: an unqualified name, such as
- PredefinedTypeExpr: a predefined type, such as
number
,string
,void
, orany
. - ThisTypeExpr: the
this
type. - InterfaceTypeExpr, also known as a literal type, such as
{x: number}
. - FunctionTypeExpr: a type such as
(x: number) => string
. - GenericTypeExpr: a named type with type arguments, such as
Array<string>
. - LiteralTypeExpr: a string, number, or boolean constant used as a type, such as
'foo'
. - ArrayTypeExpr: a type such as
string[]
. - UnionTypeExpr: a type such as
string | number
. - IntersectionTypeExpr: a type such as
S & T
. - IndexedAccessTypeExpr: a type such as
T[K]
. - ParenthesizedTypeExpr: a type such as
(string)
. - TupleTypeExpr: a type such as
[string, number]
. - KeyofTypeExpr: a type such as
keyof T
. - TypeofTypeExpr: a type such as
typeof x
. - IsTypeExpr: a type such as
x is string
. - MappedTypeExpr: a type such as
{ [K in C]: T }
.
There are some subclasses that may be part of a type annotation, but are not themselves types:
- TypeParameter: a type parameter declared on a type or function, such as
T
inclass C<T> {}
. - NamespaceAccess: a name referring to a namespace from inside a type, such as
http
inhttp.ServerRequest
.- LocalNamespaceAccess: the initial identifier in a prefix, such as
http
inhttp.ServerRequest
. - QualifiedNamespaceAccess: a qualified name in a prefix, such as
net.client
innet.client.Connection
. - ImportNamespaceAccess: an
import
used as a namespace in a type, such as inimport("http").ServerRequest
.
- LocalNamespaceAccess: the initial identifier in a prefix, such as
- VarTypeAccess: a reference to a value from inside a type, such as
x
intypeof x
orx is string
.
Function signatures¶
The Function class is a broad class that includes both concrete functions and function signatures.
Function signatures can take several forms:
- Function types, such as
(x: number) => string
. - Abstract methods, such as
abstract foo(): void
. - Overload signatures, such as
foo(x: number): number
followed by an implementation offoo
. - Call signatures, such as in
{ (x: string): number }
. - Index signatures, such as in
{ [x: string]: number }
. - Functions in an ambient context, such as
declare function foo(x: number): string
.
We recommend that you use the predicate Function.hasBody()
to distinguish concrete functions from signatures.
Type parameters¶
The TypeParameter class represents type parameters, and the TypeParameterized class represents entities that can declare type parameters. Classes, interfaces, type aliases, functions, and mapped type expressions are all TypeParameterized
.
You can access type parameters using the following predicates:
TypeParameterized.getTypeParameter(n)
gets then
th declared type parameter.TypeParameter.getHost()
gets the entity declaring a given type parameter.
You can access type arguments using the following predicates:
GenericTypeExpr.getTypeArgument(n)
gets then
th type argument of a type.TypeAccess.getTypeArgument(n)
is a convenient alternative for the above (a TypeAccess with type arguments is wrapped in a GenericTypeExpr).InvokeExpr.getTypeArgument(n)
gets then
th type argument of a call.ExpressionWithTypeArguments.getTypeArgument(n)
gets then
th type argument of a generic superclass expression.
To select references to a given type parameter, use getLocalTypeName()
(see Name binding below).
Examples¶
Select expressions that cast a value to a type parameter:
import javascript
from TypeParameter param, TypeAssertion assertion
where assertion.getTypeAnnotation() = param.getLocalTypeName().getAnAccess()
select assertion, "Cast to type parameter."
Classes and interfaces¶
The CodeQL class ClassOrInterface is a common supertype of classes and interfaces, and provides some TypeScript-specific member predicates:
ClassOrInterface.isAbstract()
holds if this is an interface or a class with theabstract
modifier.ClassOrInterface.getASuperInterface()
gets a type from theimplements
clause of a class or from theextends
clause of an interface.ClassOrInterface.getACallSignature()
gets a call signature of an interface, such as in{ (arg: string): number }
.ClassOrInterface.getAnIndexSignature()
gets an index signature, such as in{ [key: string]: number }
.ClassOrInterface.getATypeParameter()
gets a declared type parameter (special case ofTypeParameterized.getATypeParameter()
).
Note that the superclass of a class is an expression, not a type annotation. If the superclass has type arguments, it will be an expression of kind ExpressionWithTypeArguments.
Also see the documentation for classes in the “CodeQL libraries for JavaScript.”
To select the type references to a class or an interface, use getTypeName()
.
Statements¶
The following are TypeScript-specific statements:
- NamespaceDeclaration: a statement such as
namespace M {}
. - EnumDeclaration: a statement such as
enum Color { red, green, blue }
. - TypeAliasDeclaration: a statement such as
type A = number
. - InterfaceDeclaration: a statement such as
interface Point { x: number; y: number; }
. - ImportEqualsDeclaration: a statement such as
import fs = require("fs")
. - ExportAssignDeclaration: a statement such as
export = M
. - ExportAsNamespaceDeclaration: a statement such as
export as namespace M
. - ExternalModuleDeclaration: a statement such as
module "foo" {}
. - GlobalAugmentationDeclaration: a statement such as
global {}
Expressions¶
The following are TypeScript-specific expressions:
- ExpressionWithTypeArguments: occurs when the
extends
clause of a class has type arguments, such as inclass C extends D<string>
. - TypeAssertion: asserts that a value has a given type, such as
x as number
or<number> x
. - NonNullAssertion: asserts that a value is not null or undefined, such as
x!
. - ExternalModuleReference: a
require
call on the right-hand side of an import-assign, such asimport fs = require("fs")
.
Ambient declarations¶
Type annotations, interfaces, and type aliases are considered ambient AST nodes, as is anything with a declare
modifier.
The predicate ASTNode.isAmbient()
can be used to determine if an AST node is ambient.
Ambient nodes are mostly ignored by control flow and data flow analysis. The outermost part of an ambient declaration has a single no-op node in the control flow graph, and it has no internal control flow.
Static type information¶
Static type information and global name binding is available for projects with “full” TypeScript extraction enabled. This option is enabled by default when you create databases with the CodeQL CLI.
Basic usage¶
The Type class represents a static type, such as number
or string
. The type of an expression can be obtained with Expr.getType()
.
Types that refer to a specific named type can be recognized in various ways:
type.(TypeReference).hasQualifiedName(name)
holds if the type refers to the given named type.type.(TypeReference).hasUnderlyingType(name)
holds if the type refers to the given named type or a transitive subtype thereof.type.hasUnderlyingType(name)
is like the above, but additionally holds if the reference is wrapped in a union and/or intersection type.
The hasQualifiedName
and hasUnderlyingType
predicates have two overloads:
- The single-argument version takes a qualified name relative to the global scope.
- The two-argument version takes the name of a module and qualified name relative to that module.
Example¶
The following query can be used to find all toString
calls on a Node.js Buffer
object:
import javascript
from MethodCallExpr call
where call.getReceiver().getType().hasUnderlyingType("Buffer")
and call.getMethodName() = "toString"
select call
Working with types¶
Type
entities are not associated with a specific source location. For instance, there can be many uses of the number
keyword, but there is only one number
type.
Some important member predicates of Type
are:
Type.getProperty(name)
gets the type of a named property.Type.getMethod(name)
gets the signature of a named method.Type.getSignature(kind,n)
gets then
th overload of a call or constructor signature.Type.getStringIndexType()
gets the type of the string index signature.Type.getNumberIndexType()
gets the type of the number index signature.
A Type
entity always belongs to exactly one of the following subclasses:
TypeReference
: a named type, possibly with type arguments.UnionType
: a union type such asstring | number
.IntersectionType
: an intersection type such asT & U
.TupleType
: a tuple type such as[string, number]
.StringType
: thestring
type.NumberType
: thenumber
type.AnyType
: theany
type.NeverType
: thenever
type.VoidType
: thevoid
type.NullType
: thenull
type.UndefinedType
: theundefined
type.ObjectKeywordType
: theobject
type.SymbolType
: asymbol
orunique symbol
type.AnonymousInterfaceType
: an anonymous type such as{x: number}
.TypeVariableType
: a reference to a type variable.ThisType
: thethis
type within a specific type.TypeofType
: the type of a named value, such astypeof X
.BooleanLiteralType
: thetrue
orfalse
type.StringLiteralType
: the type of a string constant.NumberLiteralType
: the type of a number constant.
Additionally, Type
has the following subclasses which overlap partially with those above:
BooleanType
: the typeboolean
, internally represented as the union typetrue | false
.PromiseType
: a type that describes a promise such asPromise<T>
.ArrayType
: a type that describes an array object, possibly a tuple type.PlainArrayType
: a type of formArray<T>
.ReadonlyArrayType
: a type of formReadonlyArray<T>
.
LiteralType
: a boolean, string, or number literal type.NumberLikeType
: thenumber
type or a number literal type.StringLikeType
: thestring
type or a string literal type.BooleanLikeType
: thetrue
,false
, orboolean
type.
Canonical names and named types¶
CanonicalName
is a CodeQL class representing a qualified name relative to a root scope, such as a module or the global scope. It typically represents an entity such as a type, namespace, variable, or function. TypeName
and Namespace
are subclasses of this class.
Canonical names can be recognized using the hasQualifiedName
predicate:
hasQualifiedName(name)
holds if the qualified name isname
relative to the global scope.hasQualifiedName(module,name)
holds if the qualified name isname
relative to the given module name.
For convenience, this predicate is also available on other classes, such as TypeReference
and TypeofType
, where it forwards to the underlying canonical name.
Function types¶
There is no CodeQL class for function types, as any type with a call or construct signature is usable as a function. The type CallSignatureType
represents such a signature (with or without the new
keyword).
Signatures can be obtained in several ways:
Type.getFunctionSignature(n)
gets then
th overloaded function signature.Type.getConstructorSignature(n)
gets then
th overloaded constructor signature.Type.getLastFunctionSignature()
gets the last declared function signature.Type.getLastConstructorSignature()
gets the last declared constructor signature.
Some important member predicates of CallSignatureType
are:
CallSignatureType.getParameter(n)
gets the type of then
th parameter.CallSignatureType.getParameterName(n)
gets the name of then
th parameter.CallSignatureType.getReturnType()
gets the return type.
Note that a signature is not associated with a specific declaration site.
Call resolution¶
Additional type information is available for invocation expressions:
InvokeExpr.getResolvedCallee()
gets the callee as a concreteFunction
.InvokeExpr.getResolvedCalleeName()
get the callee as a canonical name.InvokeExpr.getResolvedSignature()
gets the signature of the invoked function, with overloading resolved and type arguments substituted.
Note that these refer to the call target as determined by the type system. The actual call target may differ at runtime, for instance, if the target is a method that has been overridden in a subclass.
Inheritance and subtyping¶
The declared supertypes of a named type can be obtained using TypeName.getABaseTypeName()
.
This operates at the level of type names, hence the specific type arguments used in the inheritance chain are not available. However, these can often be deduced using Type.getProperty
or Type.getMethod
which both take inheritance into account.
This only accounts for types explicitly mentioned in the extends
or implements
clause of a type. There is no predicate that determines subtyping or assignability between types in general.
The following two predicates can be useful for recognising subtypes of a given type:
Type.unfold()
unfolds unions and/or intersection types and get the underlying types, or the type itself if it is not a union or intersection.Type.hasUnderlyingType(name)
holds if the type is a reference to the given named type, possibly after unfolding unions/intersections and following declared supertypes.
Example¶
The following query can be used to find all classes that are React components, along with the type of their props
property, which generally coincides with its first type argument:
import javascript
from ClassDefinition cls, TypeName name
where name = cls.getTypeName()
and name.getABaseTypeName+().hasQualifiedName("React.Component")
select cls, name.getType().getProperty("props")
Name binding¶
In TypeScript, names can refer to variables, types, and namespaces, or a combination of these.
These concepts are modeled as distinct entities: Variable, TypeName, and Namespace. For example, the class C
below introduces both a variable and a type:
class C {}
let x = C; // refers to the variable C
let y: C; // refers to the type C
The variable C
and the type C
are modeled as distinct entities. One is a Variable, the other is a TypeName.
TypeScript also allows you to import types and namespaces, and give them local names in different scopes. For example, the import below introduces a local type name B
:
import {C as B} from "./foo"
The local name B
is represented as a LocalTypeName named B
, restricted to just the file containing the import. An import statement can also introduce a Variable and a LocalNamespaceName.
The following table shows the relevant classes for working with each kind of name. The classes are described in more detail below.
Kind | Local alias | Canonical name | Definition | Access |
---|---|---|---|---|
Value | Variable | VarAccess | ||
Type | LocalTypeName | TypeName | TypeDefinition | TypeAccess |
Namespace | LocalNamespaceName | Namespace | NamespaceDefinition | NamespaceAccess |
Note: TypeName
and Namespace
are only populated if the database is generated using full TypeScript extraction. LocalTypeName
and LocalNamespaceName
are always populated.
Type names¶
A TypeName is a qualified name for a type and is not bound to a specific lexical scope. The TypeDefinition class represents an entity that defines a type, namely a class, interface, type alias, enum, or enum member. The relevant predicates for working with type names are:
TypeAccess.getTypeName()
gets the qualified name being referenced (if any).TypeDefinition.getTypeName()
gets the qualified name of a class, interface, type alias, enum, or enum member.TypeName.getAnAccess()
, gets an access to a given type.TypeName.getADefinition()
, get a definition of a given type. Note that interfaces can have multiple definitions.
A LocalTypeName behaves like a block-scoped variable, that is, it has an unqualified name and is restricted to a specific scope. The relevant predicates are:
LocalTypeAccess.getLocalTypeName()
gets the local name referenced by an unqualified type access.LocalTypeName.getAnAccess()
gets an access to a local type name.LocalTypeName.getADeclaration()
gets a declaration of this name.LocalTypeName.getTypeName()
gets the qualified name to which this name refers.
Examples¶
Find references that omit type arguments to a generic type.
It is best to use TypeName to resolve through imports and qualified names:
import javascript
from TypeDefinition def, TypeAccess access
where access.getTypeName().getADefinition() = def
and def.(TypeParameterized).hasTypeParameters()
and not access.hasTypeArguments()
select access, "Type arguments are omitted"
Find imported names that are used as both a type and a value:
import javascript
from ImportSpecifier spec
where exists (LocalTypeAccess access | access.getLocalTypeName().getADeclaration() = spec.getLocal())
and exists (VarAccess access | access.getVariable().getADeclaration() = spec.getLocal())
select spec, "Used as both variable and type"
Namespace names¶
Namespaces are represented by the classes Namespace and LocalNamespaceName. The NamespaceDefinition class represents a syntactic definition of a namespace, which includes ordinary namespace declarations as well as enum declarations.
Note that these classes deal exclusively with namespaces referenced from inside type annotations, not through expressions.
A Namespace is a qualified name for a namespace, and is not bound to a specific scope. The relevant predicates for working with namespaces are:
NamespaceAccess.getNamespace()
gets the namespace being referenced by a namespace access.NamespaceDefinition.getNamespace()
gets the namespace defined by a namespace or enum declaration.Namespace.getAnAccess()
gets an access to a namespace from inside a type.Namespace.getADefinition()
gets a definition of this namespace. Note that namespaces can have multiple definitions.Namespace.getNamespaceMember(name)
gets an inner namespace with a given name.Namespace.getTypeMember(name)
gets a type exported under a given name.Namespace.getAnExportingContainer()
gets a StmtContainer whose exports contribute to this namespace. This can be a the body of a namespace declaration or the top-level of a module. Enums have no exporting containers.
A LocalNamespaceName behaves like a block-scoped variable, that is, it has an unqualified name and is restricted to a specific scope. The relevant predicates are:
LocalNamespaceAccess.getLocalNamespaceName()
gets the local name referenced by an identifier.LocalNamespaceName.getAnAccess()
gets an identifier that refers to this local name.LocalNamespaceName.getADeclaration()
gets an identifier that declares this local name.LocalNamespaceName.getNamespace()
gets the namespace to which this name refers.