Classification of types in TypeScript
Types are classified into two main classes in TypeScript:
Basic or Primitive Types
boolean
number
string
array:
number[]
,Array<number>
tuple:
[string, number]
enum:
enum Color {Red, Green = 2, Blue}
,let c : Color = Color.blue
null
undefined
any
void: exists purely to indicate the absence of a value, such as in a function with no return value.
Complex or Non-Primitive Types
classes
interface
Type Assertions
Sometimes you’ll end up in a situation where you’ll know more about a value than TypeScript does. Usually this will happen when you know the type of some entity could be more specific than its current type.
1 | let someValue : any = 'this is a string' |
Complex Types in TypeScript
Interface
Class Types
Function Types
Indexable Types
Classes
Excess Property
If SquareConfig
can have color
and width
propertes, but could also have any number of other properties, then we could define it like so:
1 | interface SquareConfig { |
Class Types
Implementing an interface
One of the most common use of interfaces in language like C# and java, that of explicitly enforcing that a class meets a particular contract, is also possible in TypeScript.
1 | interface ClockInterface { |
Difference between the static and instance sides of classes
When working with classes and interfaces, it helps to keep in mind that a class has two
types: the type of the static side and the type of the instance side.
You may notice that if you create an interface with a constructor signature and try to create a class that implements this interface you get an error:
This is because when a class implements an interface, only the instance side of the class is checked. Since the constructor sits in the static side, it is not included in this check.
Instead, you would need to work with the static side of the class directly. In this example, we define two interfaces, ClockConstructor
for the constructor and Clockinterface
for the instance methods.
1 | interface ClockConstructor { |
Function Types
To describe a function type with an interface, we give the interface a call signature. This is like a function declaration with only the parameter list and return type given. Each parameter in the parameters list requires both name and type.
1 | interface SearchFunc { |
Once defined, we can use this function type interface like we would other interfaces.
1 | let mySearch: SearchFunc |
For function types to correctly type-check, the names of the parameters do not need to match. We could have, for example, written the above example like this:
1 | let mySearch: SearchFunc |
Function parameters are checked one at a time, with the type in each corresponding parameter position checked against each other. If you do not want to specify types at all, TypeScript’s contextual typing can infer the argument types since the function value is assigned directly to a variable of type SearchFunc
1 | let mySearch: SearchFunc |
Indexable Types
Intexable types have an index signature that describes the types we can use to index into the object, along with the corresponding return types when indexing.
1 | interface StringArray { |
Above we have a StringArray interface that has an index signature. This index signature states that when a StringArray is indexed with a number, it will return a string.
There are two types of supported index signature: string an number. It is possible to support both types of indexer, but the type returned from a numeric indexer must be a subtype of the type returned from the string indexer. This is because when indexing witha number, javascript will actually convert that to a string before indexing into a object.
Classes
1 | class Greeter { |
This class has three members: a property called greeting
, a constructor, and a method greet
.
In the last line we construct an instance of the Greeter class using new
. This calls into the constructor we defined earlier, creating a new object with the Greeter shape, and running the constructor to initialize it.
Public, Private, and Protected Modifiers
Public by default.
When a member is marked private
, it cannot be accessed from outside of its containing class.
When comparing types that have private
and protected
members, TypeScript treats these types differently. For two types to be considered compatible, if one of them has a private
member, then the other must have a private
member that originated in the same declaration.
The protected
modifier acts much like the private
modifier with the exectpion the members declared protected
can also be accessed by instance of deriving classes.
A constructor may also be marked protected
. This means that the class cannot be instantiated outside of its containing class, but can be extended.
1 | class Person { |
You can make properties readonly by using the readonly
keyword. Readonly properties must be initilized at their declaration or in the constructor.
Parameter Propertes
Parameter Properties let you create and initialize a member in one place.
Accessors
TypeScript supports getter/setters as a way of intercepting accesses to a member of an object. This gives you a way of having finer-grained control over how a member is accessed on each object.
1 | let passcode = 'secret passcode' |
Accessors with a get and no set are automatically inferred to be readonly
.
Static Properties
Static Members of a class are those visible on class itself rather than on the instance.
1 | class Grid { |
Abstract Classes
Abstract Classes are base classes from which other classes may be derived. They may not be instantiated directly. Unlike an interface, an abstract class may contain implementation details for its members. The abstract
keyword is used to define abstract classes as well as abstract methods within an abstract class.
1 | abstract class Animal { |
Methods within an abstract class that are marked as abstract do not contain an implementation and must be implemented in derived classes. Abstract methods share a similar syntax to interface methods. Both define the signature of a method without including a method body. However, abstract methods must include the abstract
keyword and may optionally include access modifier.
1 | abstract class Department { |
Advanced Techniques
Constructor functions
When you declare a class in TypeScript, you are actually creating multiple delcaration at the same time. The first is the type of the instance
of the class
1 | class Greeter { |
Here, when we say let greeter: Greeter
we’re using Greeter
as the type of instance of the class Greeter
. This is almost second nature to programmers from other oo language.
We’re also creating another value that we call the constructor function
. This is the function that is called when we new
up instance of the class. To see what this looks in practice, let’s take a look at the JavaScript created the above example:
1 | let Greeter = (function () { |
Here let Greeter
is going to be assigned the constructor function. When we call new
and run this function, we get an instance of the class. The constructor function also contains all of the static members of the class. Another way to think of each class is that there is an instance side and a static side.
Using a class as an interface
As mentioned above, a class declaration creates two things: a type representing instances of the class, and a constructor function. Because classes creates types, you can use them in the same places you would be able to use interfaces:
1 | class Point { |
Inheritance
In TypeScript, we can use common object-orientad pattern. Of course, one of the most fundamental patterns in class-based programming is being able to extend existing classes to create new ones using inheritance.
1 | class Animal { |
Derived classes that contain constructor functions must call super()
which will execute the constructor function on the base class.
Interfaces Extending Classes
When an interface type extends a class type it inherits the memembers of the class but not their implementations. It is as if the interface had declared all of the members of the class without providing an implementation. Interface inherit event the private and protected members of a base class. This means that when you create an interface that extends a class with private or protected memebrs, that interface type can only be implemented by the class or a subclass of it.
This is useful when you have a large inheritance hierarchy, but want to specify that you code works iwth only subclasses that have certain properties. The subclasses don’t have to be related besides inheriting from the base class.
1 | class Control { |
In the above example, SelectableControl
contains all of the members of Control, including the private state
property. Since state
is a private member it is only possible for descendants of Control to implement SelectableControl
. This is because only descendants of Control will have a state
private member that originates in the same declaration, which is a requirement for private members to be compatible.
Within the Control
class it is possible to access the state
private member through an instance of SelectableControl
. Effectively, a SelectableControl
acts like a Control
that is known to have a select
method. The Button
and TextBox
classes are subtypes of SelectableControl
, but Image
and Location
are not.
Type Compatibility
Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based on solely on their members. This is in contrast with normal typing.
1 | interface Named { |
A basic rule for TypeScript’s structural type system is that x is compatible with y if y has at least the same members as x.
1 | interface Named { |
Comparing Two Functions
While comparing primitive types and object types is relatively straightforward, the question of what kinds of functions should be considered compatible is a bit more involved.
1 | let x = (a: number) => 0; |
To check if x is assignable to y, we first look at the parameter list. Each parameter in x must have a corresponding parameter in y with a compatible type. Note that the name of the parameters are not considered, only their types. In this case, every parameter of x has a corresponding compatible parameter in y, so x is assignable to y.
To ensure that every parameter in x can be checked in y.
Enums
Enums are compatible with numbers, and numers are compatible with enums.
Enum values from different enum types are considered incompatible.
Classes
Classes work similarly to object literal types and interfaces with one exception: they have both a static and an instance type. When comparing two objects of a class type, only members of the instance are compared. Static memebrs and constructor do not affect compatibility.
1 | class Animal { |
Private and protected members in a class affect their compatibility. When an instance of a class is checked for compatibility, if the target type contains a private member, then the source type must also contain a private member that originated from the same class.
Contextual Type
Type inference also work in ‘the other direction’ in some cases in TypeScript. This is known as ‘contextual typing’. Contextual typing occurs when the type of an expression is implied by its location.
1 | window.onmousedown = function (mouseEvent) { |
Functions and function Types
TypeScript functions can be created both as a named function or as an anonymous function. This allows you to choose the most appropriate approach for your application, whether you’re building a list of functions in an API or a one-off function to hand off to another function.
1 | // Named Function |
Function Types
Typing the function.
1 | function add(x: number, y: number): number { |
Writing the function type
1 | let myAdd: (x: number, y: number) => number = function(x, y) { return x + y } |
A function’s type has the same two parts: the type of the arguments and the return type. When writing out the whole function type, both parts are required.
Context and Scope
this
and arrow function
.
In JavaScript, this
is a variable that’s set when a function is called. This makes it a very powerful and flexible feature, but it comes at the cost of always having to know about the context that a function is executing in.
1 | let deck = { |
Here the function returned by createCardPicker
will be called by window
1 | let deck { |
Overloads
JavaScript is inherently a very dynamic language. It’s not uncommon for a single JavaScript function to return different types of objects based on the shape of the arguments passed in.
1 | let suits = ['hearts', 'spades', 'clubs', 'diamonds'] |
Here the pickCard
function will return two different things based on what the user ahs passed in.
Use overloads.
1 | let suits = ['hearts', 'spades', 'clubs', 'diamonds'] |
Note that the function pickCard(x): any
is not part of the overload list.
Namespaces
Namespaces are used as a way to organize the code so that you can keep track of your types and not worry about name collisions with other object. Instead of putting lots of different names into the global namespace, you can wrap up your object into a namespace. The choice of what objects go in what namespaces is totally up to your organization preference.
If you want the interfaces and classes in your namespaces to be visible outside the namespace, you need to preface their declaration with export
keyword.
Also keep in mind that classes, interfaces and variable names within a namespace has to be unique.
Multi-file namespaces
You can split a namespace across many files, even though the fiels a separate, they can each contribute to the same namespace and can be consumed as if they were all defined in one place. Because there are dependencies between files. you will add reference
tags to tell the compiler about the relationship between the files.
1 | // ZooAnimals.ts |
Once there are multiple fiels involved, we’ll need to make sure all of the compiled code gets loaded, we can use concatenated output using the --outFile
flag to compile all of the input files into a single JavaScript output file.
1 | tsc --outFile zoo.js ZooAnimal.ts ZooWild.ts ZooBirds.ts |
Referencing namespacing entities in your code
Namespaces can be accessed either in the same file where the namespace is defined or another .ts file. The only control on access is whether you are using the export
keyword or not for the namespace entities.
1 | let parrot = new Zoo.Bird(); |
Aliases
You can simplify working with namespaces by using Aliases. You can use import q = x.y.z
to create shorter names for commonly-used objects.
1 | import rep = Zoo.Reptitle; |