Typescript Basic Type and Advance
- Introduction
- Boolean
- Number
- String
- Array
- Tuple
- Enum
- Any
- Void
- Null and Undefined
- Never
- Object
- Type assertions
- A note about 'let'
Introduction
For programs to be useful, we need to be able to work with some of the simplest units of data: numbers, strings, structures, boolean values, and the like. In TypeScript, we support much the same types as you would expect in JavaScript, with a convenient enumeration type thrown in to help things along.
Boolean
The most basic datatype is the simple true/false value, which JavaScript and TypeScript call a boolean
value.
let isDone: boolean = false;
Number
As in JavaScript, all numbers in TypeScript are floating point values.
These floating point numbers get the type number
.
In addition to hexadecimal and decimal literals, TypeScript also supports binary and octal literals introduced in ECMAScript 2015.
let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;
String
Another fundamental part of creating programs in JavaScript for webpages and servers alike is working with textual data.
As in other languages, we use the type string
to refer to these textual datatypes.
Just like JavaScript, TypeScript also uses double quotes ("
) or single quotes ('
) to surround string data.
let color: string = "blue";
color = 'red';
You can also use template strings, which can span multiple lines and have embedded expressions.
These strings are surrounded by the backtick/backquote (`
) character, and embedded expressions are of the form ${ expr }
.
let fullName: string = `Bob Bobbington`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ fullName }.
I'll be ${ age + 1 } years old next month.`;
This is equivalent to declaring sentence
like so:
let sentence: string = "Hello, my name is " + fullName + ".\n\n" +
"I'll be " + (age + 1) + " years old next month.";
Array
TypeScript, like JavaScript, allows you to work with arrays of values.
Array types can be written in one of two ways.
In the first, you use the type of the elements followed by []
to denote an array of that element type:
let list: number[] = [1, 2, 3];
The second way uses a generic array type, Array<elemType>
:
let list: Array<number> = [1, 2, 3];
Tuple
Tuple types allow you to express an array with a fixed number of elements whose types are known, but need not be the same. For example, you may want to represent a value as a pair of a string
and a number
:
// Declare a tuple type
let x: [string, number];
// Initialize it
x = ["hello", 10]; // OK
// Initialize it incorrectly
x = [10, "hello"]; // Error
When accessing an element with a known index, the correct type is retrieved:
console.log(x[0].substring(1)); // OK
console.log(x[1].substring(1)); // Error, 'number' does not have 'substring'
Accessing an element outside the set of known indices fails with an error:
x[3] = "world"; // Error, Property '3' does not exist on type '[string, number]'.
console.log(x[5].toString()); // Error, Property '5' does not exist on type '[string, number]'.
Enum
A helpful addition to the standard set of datatypes from JavaScript is the enum
.
As in languages like C#, an enum is a way of giving more friendly names to sets of numeric values.
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
By default, enums begin numbering their members starting at 0
.
You can change this by manually setting the value of one of its members.
For example, we can start the previous example at 1
instead of 0
:
enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;
Or, even manually set all the values in the enum:
enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;
A handy feature of enums is that you can also go from a numeric value to the name of that value in the enum.
For example, if we had the value 2
but weren't sure what that mapped to in the Color
enum above, we could look up the corresponding name:
enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName); // Displays 'Green' as its value is 2 above
Any
We may need to describe the type of variables that we do not know when we are writing an application.
These values may come from dynamic content, e.g. from the user or a 3rd party library.
In these cases, we want to opt-out of type checking and let the values pass through compile-time checks.
To do so, we label these with the any
type:
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // okay, definitely a boolean
The any
type is a powerful way to work with existing JavaScript, allowing you to gradually opt-in and opt-out of type checking during compilation.
You might expect Object
to play a similar role, as it does in other languages.
However, variables of type Object
only allow you to assign any value to them. You can't call arbitrary methods on them, even ones that actually exist:
let notSure: any = 4;
notSure.ifItExists(); // okay, ifItExists might exist at runtime
notSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)
let prettySure: Object = 4;
prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
Note: Avoid using
Object
in favor of the non-primitiveobject
type as described in our Do's and Don'ts section.
The any
type is also handy if you know some part of the type, but perhaps not all of it.
For example, you may have an array but the array has a mix of different types:
let list: any[] = [1, true, "free"];
list[1] = 100;
Void
void
is a little like the opposite of any
: the absence of having any type at all.
You may commonly see this as the return type of functions that do not return a value:
function warnUser(): void {
console.log("This is my warning message");
}
Declaring variables of type void
is not useful because you can only assign null
(only if --strictNullChecks
is not specified, see next section) or undefined
to them:
let unusable: void = undefined;
unusable = null; // OK if `--strictNullChecks` is not given
Null and Undefined
In TypeScript, both undefined
and null
actually have their own types named undefined
and null
respectively.
Much like void
, they're not extremely useful on their own:
// Not much else we can assign to these variables!
let u: undefined = undefined;
let n: null = null;
By default null
and undefined
are subtypes of all other types.
That means you can assign null
and undefined
to something like number
.
However, when using the --strictNullChecks
flag, null
and undefined
are only assignable to any
and their respective types (the one exception being that undefined
is also assignable to void
).
This helps avoid many common errors.
In cases where you want to pass in either a string
or null
or undefined
, you can use the union type string | null | undefined
.
Union types are an advanced topic that we'll cover in a later chapter.
As a note: we encourage the use of
--strictNullChecks
when possible, but for the purposes of this handbook, we will assume it is turned off.
Never
The never
type represents the type of values that never occur.
For instance, never
is the return type for a function expression or an arrow function expression that always throws an exception or one that never returns;
Variables also acquire the type never
when narrowed by any type guards that can never be true.
The never
type is a subtype of, and assignable to, every type; however, no type is a subtype of, or assignable to, never
(except never
itself).
Even any
isn't assignable to never
.
Some examples of functions returning never
:
// Function returning never must have unreachable end point
function error(message: string): never {
throw new Error(message);
}
// Inferred return type is never
function fail() {
return error("Something failed");
}
// Function returning never must have unreachable end point
function infiniteLoop(): never {
while (true) {
}
}
Object
object
is a type that represents the non-primitive type, i.e. anything that is not number
, string
, boolean
, bigint
, symbol
, null
, or undefined
.
With object
type, APIs like Object.create
can be better represented. For example:
declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error
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.
Type assertions are a way to tell the compiler "trust me, I know what I'm doing." A type assertion is like a type cast in other languages, but performs no special checking or restructuring of data. It has no runtime impact, and is used purely by the compiler. TypeScript assumes that you, the programmer, have performed any special checks that you need.
Type assertions have two forms. One is the "angle-bracket" syntax:
let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;
And the other is the as
-syntax:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
The two samples are equivalent.
Using one over the other is mostly a choice of preference; however, when using TypeScript with JSX, only as
-style assertions are allowed.
A note about let
You may've noticed that so far, we've been using the let
keyword instead of JavaScript's var
keyword which you might be more familiar with.
The let
keyword was introduced to JavaScript in ES2015 and is now considered the standard because it's safer than var
.
We'll discuss the details later, but many common problems in JavaScript are alleviated by using let
, so you should use it instead of var
whenever possible.
Introduction
Traditional JavaScript uses functions and prototype-based inheritance to build up reusable components, but this may feel a bit awkward to programmers more comfortable with an object-oriented approach, where classes inherit functionality and objects are built from these classes. Starting with ECMAScript 2015, also known as ECMAScript 6, JavaScript programmers will be able to build their applications using this object-oriented class-based approach. In TypeScript, we allow developers to use these techniques now, and compile them down to JavaScript that works across all major browsers and platforms, without having to wait for the next version of JavaScript.
Classes
Let's take a look at a simple class-based example:
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter = new Greeter("world");
The syntax should look familiar if you've used C# or Java before.
We declare a new class Greeter
. This class has three members: a property called greeting
, a constructor, and a method greet
.
You'll notice that in the class when we refer to one of the members of the class we prepend this.
.
This denotes that it's a member access.
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.
Inheritance
In TypeScript, we can use common object-oriented patterns. One of the most fundamental patterns in class-based programming is being able to extend existing classes to create new ones using inheritance.
Let's take a look at an example:
class Animal {
move(distanceInMeters: number = 0) {
console.log(`Animal moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
bark() {
console.log('Woof! Woof!');
}
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();
This example shows the most basic inheritance feature: classes inherit properties and methods from base classes.
Here, Dog
is a derived class that derives from the Animal
base class using the extends
keyword.
Derived classes are often called subclasses, and base classes are often called superclasses.
Because Dog
extends the functionality from Animal
, we were able to create an instance of Dog
that could both bark()
and move()
.
Let's now look at a more complex example.
class Animal {
name: string;
constructor(theName: string) { this.name = theName; }
move(distanceInMeters: number = 0) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
class Snake extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 5) {
console.log("Slithering...");
super.move(distanceInMeters);
}
}
class Horse extends Animal {
constructor(name: string) { super(name); }
move(distanceInMeters = 45) {
console.log("Galloping...");
super.move(distanceInMeters);
}
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);
This example covers a few other features we didn't previously mention.
Again, we see the extends
keywords used to create two new subclasses of Animal
: Horse
and Snake
.
One difference from the prior example is that each derived class that contains a constructor function must call super()
which will execute the constructor of the base class.
What's more, before we ever access a property on this
in a constructor body, we have to call super()
.
This is an important rule that TypeScript will enforce.
The example also shows how to override methods in the base class with methods that are specialized for the subclass.
Here both Snake
and Horse
create a move
method that overrides the move
from Animal
, giving it functionality specific to each class.
Note that even though tom
is declared as an Animal
, since its value is a Horse
, calling tom.move(34)
will call the overriding method in Horse
:
Slithering...
Sammy the Python moved 5m.
Galloping...
Tommy the Palomino moved 34m.
Public, private, and protected modifiers
Public by default
In our examples, we've been able to freely access the members that we declared throughout our programs.
If you're familiar with classes in other languages, you may have noticed in the above examples we haven't had to use the word public
to accomplish this; for instance, C# requires that each member be explicitly labeled public
to be visible.
In TypeScript, each member is public
by default.
You may still mark a member public
explicitly.
We could have written the Animal
class from the previous section in the following way:
class Animal {
public name: string;
public constructor(theName: string) { this.name = theName; }
public move(distanceInMeters: number) {
console.log(`${this.name} moved ${distanceInMeters}m.`);
}
}
ECMAScript Private Fields
With TypeScript 3.8, TypeScript supports the new JavaScript syntax for private fields:
class Animal {
#name: string;
constructor(theName: string) { this.#name = theName; }
}
new Animal("Cat").#name; // Property '#name' is not accessible outside class 'Animal' because it has a private identifier.
This syntax is built into the JavaScript runtime and can have better guarantees about the isolation of each private field. Right now, the best documentation for these private fields is in the TypeScript 3.8 release notes.
Understanding TypeScript's private
TypeScript also has it's own way to declare a member as being marked private
, it cannot be accessed from outside of its containing class. For example:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // Error: 'name' is private;
TypeScript is a structural type system. When we compare two different types, regardless of where they came from, if the types of all members are compatible, then we say the types themselves are compatible.
However, when comparing types that have private
and protected
members, we treat 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 same applies to protected
members.
Let's look at an example to better see how this plays out in practice:
class Animal {
private name: string;
constructor(theName: string) { this.name = theName; }
}
class Rhino extends Animal {
constructor() { super("Rhino"); }
}
class Employee {
private name: string;
constructor(theName: string) { this.name = theName; }
}
let animal = new Animal("Goat");
let rhino = new Rhino();
let employee = new Employee("Bob");
animal = rhino;
animal = employee; // Error: 'Animal' and 'Employee' are not compatible
In this example, we have an Animal
and a Rhino
, with Rhino
being a subclass of Animal
.
We also have a new class Employee
that looks identical to Animal
in terms of shape.
We create some instances of these classes and then try to assign them to each other to see what will happen.
Because Animal
and Rhino
share the private
side of their shape from the same declaration of private name: string
in Animal
, they are compatible. However, this is not the case for Employee
.
When we try to assign from an Employee
to Animal
we get an error that these types are not compatible.
Even though Employee
also has a private
member called name
, it's not the one we declared in Animal
.
Understanding protected
The protected
modifier acts much like the private
modifier with the exception that members declared protected
can also be accessed within deriving classes. For example,
class Person {
protected name: string;
constructor(name: string) { this.name = name; }
}
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // error
Notice that while we can't use name
from outside of Person
, we can still use it from within an instance method of Employee
because Employee
derives from Person
.
A constructor may also be marked protected
.
This means that the class cannot be instantiated outside of its containing class, but can be extended. For example,
class Person {
protected name: string;
protected constructor(theName: string) { this.name = theName; }
}
// Employee can extend Person
class Employee extends Person {
private department: string;
constructor(name: string, department: string) {
super(name);
this.department = department;
}
public getElevatorPitch() {
return `Hello, my name is ${this.name} and I work in ${this.department}.`;
}
}
let howard = new Employee("Howard", "Sales");
let john = new Person("John"); // Error: The 'Person' constructor is protected
Readonly modifier
You can make properties readonly by using the readonly
keyword.
Readonly properties must be initialized at their declaration or in the constructor.
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // error! name is readonly.
Parameter properties
In our last example, we had to declare a readonly member name
and a constructor parameter theName
in the Octopus
class. This is needed in order to have the value of theName
accessible after the Octopus
constructor is executed.
Parameter properties let you create and initialize a member in one place.
Here's a further revision of the previous Octopus
class using a parameter property:
class Octopus {
readonly numberOfLegs: number = 8;
constructor(readonly name: string) {
}
}
Notice how we dropped theName
altogether and just use the shortened readonly name: string
parameter on the constructor to create and initialize the name
member.
We've consolidated the declarations and assignment into one location.
Parameter properties are declared by prefixing a constructor parameter with an accessibility modifier or readonly
, or both.
Using private
for a parameter property declares and initializes a private member; likewise, the same is done for public
, protected
, and readonly
.
Accessors
TypeScript supports getters/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.
Let's convert a simple class to use get
and set
.
First, let's start with an example without getters and setters.
class Employee {
fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
While allowing people to randomly set fullName
directly is pretty handy, we may also want enforce some constraints when fullName
is set.
In this version, we add a setter that checks the length of the newName
to make sure it's compatible with the max-length of our backing database field. If it isn't we throw an error notifying client code that something went wrong.
To preserve existing functionality, we also add a simple getter that retrieves fullName
unmodified.
const fullNameMaxLength = 10;
class Employee {
private _fullName: string;
get fullName(): string {
return this._fullName;
}
set fullName(newName: string) {
if (newName && newName.length > fullNameMaxLength) {
throw new Error("fullName has a max length of " + fullNameMaxLength);
}
this._fullName = newName;
}
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
console.log(employee.fullName);
}
To prove to ourselves that our accessor is now checking the length of values, we can attempt to assign a name longer than 10 characters and verify that we get an error.
A couple of things to note about accessors:
First, accessors require you to set the compiler to output ECMAScript 5 or higher.
Downleveling to ECMAScript 3 is not supported.
Second, accessors with a get
and no set
are automatically inferred to be readonly
.
This is helpful when generating a .d.ts
file from your code, because users of your property can see that they can't change it.
Static Properties
Up to this point, we've only talked about the instance members of the class, those that show up on the object when it's instantiated.
We can also create static members of a class, those that are visible on the class itself rather than on the instances.
In this example, we use static
on the origin, as it's a general value for all grids.
Each instance accesses this value through prepending the name of the class.
Similarly to prepending this.
in front of instance accesses, here we prepend Grid.
in front of static accesses.
class Grid {
static origin = {x: 0, y: 0};
calculateDistanceFromOrigin(point: {x: number; y: number;}) {
let xDist = (point.x - Grid.origin.x);
let yDist = (point.y - Grid.origin.y);
return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale;
}
constructor (public scale: number) { }
}
let grid1 = new Grid(1.0); // 1x scale
let grid2 = new Grid(5.0); // 5x scale
console.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));
console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
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.
abstract class Animal {
abstract makeSound(): void;
move(): void {
console.log("roaming the earth...");
}
}
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 modifiers.
abstract class Department {
constructor(public name: string) {
}
printName(): void {
console.log("Department name: " + this.name);
}
abstract printMeeting(): void; // must be implemented in derived classes
}
class AccountingDepartment extends Department {
constructor() {
super("Accounting and Auditing"); // constructors in derived classes must call super()
}
printMeeting(): void {
console.log("The Accounting Department meets each Monday at 10am.");
}
generateReports(): void {
console.log("Generating accounting reports...");
}
}
let department: Department; // ok to create a reference to an abstract type
department = new Department(); // error: cannot create an instance of an abstract class
department = new AccountingDepartment(); // ok to create and assign a non-abstract subclass
department.printName();
department.printMeeting();
department.generateReports(); // error: method doesn't exist on declared abstract type
Advanced Techniques
Constructor functions
When you declare a class in TypeScript, you are actually creating multiple declarations at the same time. The first is the type of the instance of the class.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
let greeter: Greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"
Here, when we say let greeter: Greeter
, we're using Greeter
as the type of instances of the class Greeter
.
This is almost second nature to programmers from other object-oriented languages.
We're also creating another value that we call the constructor function.
This is the function that is called when we new
up instances of the class.
To see what this looks like in practice, let's take a look at the JavaScript created by the above example:
let Greeter = (function () {
function Greeter(message) {
this.greeting = message;
}
Greeter.prototype.greet = function () {
return "Hello, " + this.greeting;
};
return Greeter;
})();
let greeter;
greeter = new Greeter("world");
console.log(greeter.greet()); // "Hello, world"
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.
Let's modify the example a bit to show this difference:
class Greeter {
static standardGreeting = "Hello, there";
greeting: string;
greet() {
if (this.greeting) {
return "Hello, " + this.greeting;
}
else {
return Greeter.standardGreeting;
}
}
}
let greeter1: Greeter;
greeter1 = new Greeter();
console.log(greeter1.greet()); // "Hello, there"
let greeterMaker: typeof Greeter = Greeter;
greeterMaker.standardGreeting = "Hey there!";
let greeter2: Greeter = new greeterMaker();
console.log(greeter2.greet()); // "Hey there!"
In this example, greeter1
works similarly to before.
We instantiate the Greeter
class, and use this object.
This we have seen before.
Next, we then use the class directly.
Here we create a new variable called greeterMaker
.
This variable will hold the class itself, or said another way its constructor function.
Here we use typeof Greeter
, that is "give me the type of the Greeter
class itself" rather than the instance type.
Or, more precisely, "give me the type of the symbol called Greeter
," which is the type of the constructor function.
This type will contain all of the static members of Greeter along with the constructor that creates instances of the Greeter
class.
We show this by using new
on greeterMaker
, creating new instances of Greeter
and invoking them as before.
Using a class as an interface
As we said in the previous section, a class declaration creates two things: a type representing instances of the class and a constructor function. Because classes create types, you can use them in the same places you would be able to use interfaces.
class Point {
x: number;
y: number;
}
interface Point3d extends Point {
z: number;
}
let point3d: Point3d = {x: 1, y: 2, z: 3};
One of TypeScript's core principles is that type checking focuses on the shape that values have. This is sometimes called "duck typing" or "structural subtyping". In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.
Our First Interface
The easiest way to see how interfaces work is to start with a simple example:
function printLabel(labeledObj: { label: string }) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
The type checker checks the call to printLabel
.
The printLabel
function has a single parameter that requires that the object passed in has a property called label
of type string
.
Notice that our object actually has more properties than this, but the compiler only checks that at least the ones required are present and match the types required.
There are some cases where TypeScript isn't as lenient, which we'll cover in a bit.
We can write the same example again, this time using an interface to describe the requirement of having the label
property that is a string:
interface LabeledValue {
label: string;
}
function printLabel(labeledObj: LabeledValue) {
console.log(labeledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);
The interface LabeledValue
is a name we can now use to describe the requirement in the previous example.
It still represents having a single property called label
that is of type string
.
Notice we didn't have to explicitly say that the object we pass to printLabel
implements this interface like we might have to in other languages.
Here, it's only the shape that matters. If the object we pass to the function meets the requirements listed, then it's allowed.
It's worth pointing out that the type checker does not require that these properties come in any sort of order, only that the properties the interface requires are present and have the required type.
Optional Properties
Not all properties of an interface may be required. Some exist under certain conditions or may not be there at all. These optional properties are popular when creating patterns like "option bags" where you pass an object to a function that only has a couple of properties filled in.
Here's an example of this pattern:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
let newSquare = {color: "white", area: 100};
if (config.color) {
newSquare.color = config.color;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
Interfaces with optional properties are written similar to other interfaces, with each optional property denoted by a ?
at the end of the property name in the declaration.
The advantage of optional properties is that you can describe these possibly available properties while still also preventing use of properties that are not part of the interface.
For example, had we mistyped the name of the color
property in createSquare
, we would get an error message letting us know:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
let newSquare = {color: "white", area: 100};
if (config.clor) {
// Error: Property 'clor' does not exist on type 'SquareConfig'
newSquare.color = config.clor;
}
if (config.width) {
newSquare.area = config.width * config.width;
}
return newSquare;
}
let mySquare = createSquare({color: "black"});
Readonly properties
Some properties should only be modifiable when an object is first created.
You can specify this by putting readonly
before the name of the property:
interface Point {
readonly x: number;
readonly y: number;
}
You can construct a Point
by assigning an object literal.
After the assignment, x
and y
can't be changed.
let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!
TypeScript comes with a ReadonlyArray<T>
type that is the same as Array<T>
with all mutating methods removed, so you can make sure you don't change your arrays after creation:
let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!
On the last line of the snippet you can see that even assigning the entire ReadonlyArray
back to a normal array is illegal.
You can still override it with a type assertion, though:
a = ro as number[];
readonly
vs const
The easiest way to remember whether to use readonly
or const
is to ask whether you're using it on a variable or a property.
Variables use const
whereas properties use readonly
.
Excess Property Checks
In our first example using interfaces, TypeScript lets us pass { size: number; label: string; }
to something that only expected a { label: string; }
.
We also just learned about optional properties, and how they're useful when describing so-called "option bags".
However, combining the two naively would allow an error to sneak in. For example, taking our last example using createSquare
:
interface SquareConfig {
color?: string;
width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
// ...
}
let mySquare = createSquare({ colour: "red", width: 100 });
Notice the given argument to createSquare
is spelled colour
instead of color
.
In plain JavaScript, this sort of thing fails silently.
You could argue that this program is correctly typed, since the width
properties are compatible, there's no color
property present, and the extra colour
property is insignificant.
However, TypeScript takes the stance that there's probably a bug in this code. Object literals get special treatment and undergo excess property checking when assigning them to other variables, or passing them as arguments. If an object literal has any properties that the "target type" doesn't have, you'll get an error:
// error: Object literal may only specify known properties, but 'colour' does not exist in type 'SquareConfig'. Did you mean to write 'color'?
let mySquare = createSquare({ colour: "red", width: 100 });
Getting around these checks is actually really simple. The easiest method is to just use a type assertion:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
However, a better approach might be to add a string index signature if you're sure that the object can have some extra properties that are used in some special way.
If SquareConfig
can have color
and width
properties with the above types, but could also have any number of other properties, then we could define it like so:
interface SquareConfig {
color?: string;
width?: number;
[propName: string]: any;
}
We'll discuss index signatures in a bit, but here we're saying a SquareConfig
can have any number of properties, and as long as they aren't color
or width
, their types don't matter.
One final way to get around these checks, which might be a bit surprising, is to assign the object to another variable:
Since squareOptions
won't undergo excess property checks, the compiler won't give you an error.
let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);
The above workaround will work as long as you have a common property between squareOptions
and SquareConfig
.
In this example, it was the property width
. It will however, fail if the variable does not have any common object property. For example:
let squareOptions = { colour: "red" };
let mySquare = createSquare(squareOptions);
Keep in mind that for simple code like above, you probably shouldn't be trying to "get around" these checks.
For more complex object literals that have methods and hold state, you might need to keep these techniques in mind, but a majority of excess property errors are actually bugs.
That means if you're running into excess property checking problems for something like option bags, you might need to revise some of your type declarations.
In this instance, if it's okay to pass an object with both a color
or colour
property to createSquare
, you should fix up the definition of SquareConfig
to reflect that.
Function Types
Interfaces are capable of describing the wide range of shapes that JavaScript objects can take. In addition to describing an object with properties, interfaces are also capable of describing 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 parameter list requires both name and type.
interface SearchFunc {
(source: string, subString: string): boolean;
}
Once defined, we can use this function type interface like we would other interfaces. Here, we show how you can create a variable of a function type and assign it a function value of the same type.
let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
let result = source.search(subString);
return result > -1;
}
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:
let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
let result = src.search(sub);
return result > -1;
}
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
.
Here, also, the return type of our function expression is implied by the values it returns (here false
and true
).
let mySearch: SearchFunc;
mySearch = function(src, sub) {
let result = src.search(sub);
return result > -1;
}
Had the function expression returned numbers or strings, the type checker would have made an error that indicates return type doesn't match the return type described in the SearchFunc
interface.
let mySearch: SearchFunc;
// error: Type '(src: string, sub: string) => string' is not assignable to type 'SearchFunc'.
// Type 'string' is not assignable to type 'boolean'.
mySearch = function(src, sub) {
let result = src.search(sub);
return "string";
};
Indexable Types
Similarly to how we can use interfaces to describe function types, we can also describe types that we can "index into" like a[10]
, or ageMap["daniel"]
.
Indexable 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.
Let's take an example:
interface StringArray {
[index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];
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 signatures: string and number.
It is possible to support both types of indexers, 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 with a number
, JavaScript will actually convert that to a string
before indexing into an object.
That means that indexing with 100
(a number
) is the same thing as indexing with "100"
(a string
), so the two need to be consistent.
class Animal {
name: string;
}
class Dog extends Animal {
breed: string;
}
// Error: indexing with a numeric string might get you a completely separate type of Animal!
interface NotOkay {
[x: number]: Animal;
[x: string]: Dog;
}
While string index signatures are a powerful way to describe the "dictionary" pattern, they also enforce that all properties match their return type.
This is because a string index declares that obj.property
is also available as obj["property"]
.
In the following example, name
's type does not match the string index's type, and the type checker gives an error:
interface NumberDictionary {
[index: string]: number;
length: number; // ok, length is a number
name: string; // error, the type of 'name' is not a subtype of the indexer
}
However, properties of different types are acceptable if the index signature is a union of the property types:
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
Finally, you can make index signatures readonly
in order to prevent assignment to their indices:
interface ReadonlyStringArray {
readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!
You can't set myArray[2]
because the index signature is readonly.
Class Types
Implementing an interface
One of the most common uses of interfaces in languages like C# and Java, that of explicitly enforcing that a class meets a particular contract, is also possible in TypeScript.
interface ClockInterface {
currentTime: Date;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
constructor(h: number, m: number) { }
}
You can also describe methods in an interface that are implemented in the class, as we do with setTime
in the below example:
interface ClockInterface {
currentTime: Date;
setTime(d: Date): void;
}
class Clock implements ClockInterface {
currentTime: Date = new Date();
setTime(d: Date) {
this.currentTime = d;
}
constructor(h: number, m: number) { }
}
Interfaces describe the public side of the class, rather than both the public and private side. This prohibits you from using them to check that a class also has particular types for the private side of the class instance.
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 construct signature and try to create a class that implements this interface you get an error:
interface ClockConstructor {
new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
currentTime: Date;
constructor(h: number, m: number) { }
}
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.
Then, for convenience, we define a constructor function createClock
that creates instances of the type that is passed to it:
interface ClockConstructor {
new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
tick(): void;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("beep beep");
}
}
class AnalogClock implements ClockInterface {
constructor(h: number, m: number) { }
tick() {
console.log("tick tock");
}
}
let digital = createClock(DigitalClock, 12, 17);
let analog = createClock(AnalogClock, 7, 32);
Because createClock
's first parameter is of type ClockConstructor
, in createClock(AnalogClock, 7, 32)
, it checks that AnalogClock
has the correct constructor signature.
Another simple way is to use class expressions:
interface ClockConstructor {
new (hour: number, minute: number);
}
interface ClockInterface {
tick();
}
const Clock: ClockConstructor = class Clock implements ClockInterface {
constructor(h: number, m: number) {}
tick() {
console.log("beep beep");
}
}
Extending Interfaces
Like classes, interfaces can extend each other. This allows you to copy the members of one interface into another, which gives you more flexibility in how you separate your interfaces into reusable components.
interface Shape {
color: string;
}
interface Square extends Shape {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
An interface can extend multiple interfaces, creating a combination of all of the interfaces.
interface Shape {
color: string;
}
interface PenStroke {
penWidth: number;
}
interface Square extends Shape, PenStroke {
sideLength: number;
}
let square = {} as Square;
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;
Hybrid Types
As we mentioned earlier, interfaces can describe the rich types present in real world JavaScript. Because of JavaScript's dynamic and flexible nature, you may occasionally encounter an object that works as a combination of some of the types described above.
One such example is an object that acts as both a function and an object, with additional properties:
interface Counter {
(start: number): string;
interval: number;
reset(): void;
}
function getCounter(): Counter {
let counter = (function (start: number) { }) as Counter;
counter.interval = 123;
counter.reset = function () { };
return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
When interacting with 3rd-party JavaScript, you may need to use patterns like the above to fully describe the shape of the type.
Interfaces Extending Classes
When an interface type extends a class type it inherits the members 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. Interfaces inherit even 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 members, that interface type can only be implemented by that class or a subclass of it.
This is useful when you have a large inheritance hierarchy, but want to specify that your code works with only subclasses that have certain properties. The subclasses don't have to be related besides inheriting from the base class. For example:
class Control {
private state: any;
}
interface SelectableControl extends Control {
select(): void;
}
class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// Error: Property 'state' is missing in type 'Image'.
class Image implements SelectableControl {
private state: any;
select() { }
}
class Location {
}
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
(because they both inherit from Control
and have a select
method), but the Image
and Location
classes are not.
Comments