Introduction to TypeScript
Introduction to TypeScript
When you create a variable, you’re planning to give it a value. But what
kind of value it can hold depends on the variable’s data type. In
TypeScript, the type system defines the various data types supported by
the language. The data type classification is as given below:
Built-in Datatypes:
TypeScript has some pre-defined data types:
Built-in Data
Type keyword Description
Examples:
let a: null = null;
let b: number = 123;
let c: number = 123.456;
let d: string = ‘Geeks’;
let e: undefined = undefined;
let f: boolean = true;
let g: number = 0b111001; // Binary
let h: number = 0o436; // Octal
let i: number = 0xadf0d; // Hexa-Decimal
TypeScript Operators
Last Updated : 03 Sep, 2024
Performs a bitwise OR
result = operand1 |
operation between each
operand2;
Bitwise OR (|) pair of corresponding bits.
type TypeName<T> = T
Allows expressing a type extends string ?
Conditional Types based on a condition. 'string' : 'non-string';
Similar to template
let description = "I live
literals, it allows inserting in " + city + ".";
String Interpolation variables into strings.
Variables.
Syntax
TypeScript variables are generally inferred to be whatever basic type of value they are
initially assigned with. Later in code, only values that are of that same basic type may be
assigned to the variable. The term for whether a type is assignable to another type is
assignability.
myVar = 'Hello'; // Ok
myVar = 'World!'; // Also Ok
myVar = 42; // Not Ok: Type 'number' is not assignable to type
'string'.
spooky = false; // Ok
If a variable is assigned a different type of value, TypeScript will notice and emit a type
checking complaint. Those type checking complaints can be surfaced as:
In this code snippet, the scary variable is initially assigned the value "skeletons", which is
a string type. Later, assigning the decimal number 10.31 is not allowed because
a number type is not assignable to a variable of type string:
scary = 10.31;
// Error: Type 'number' is not assignable to type 'string'
Hey Dev's
How has been your week? I hope everyone had a great week. In todays
tutorial let us go through Object Oriented Programming (OOP).
greetings(){
return this.name + ' ' + this.age
}
}
1. Easier debuging
2. Reuse of code through inheritance
3. Flexibility through polymorphism
4. Effective problem solving
5. Project decoupling (Separate project into groups)
Take a look at this example. In the example we have defined two object
classes and created an instance of each. So in layman term encapsulation
principle states that the new instance of motor1 cannot access the
attributes of person1. If you try you should get such a warning.
// person object
class Person {
name: string = ''
age: number = 0
}
Abstraction.
Objects only reveal internal mechanisms that are relevant for the use of
other objects, hiding any unnecessary implementation code. This concept
helps developers more easily make changes and additions over time.
Let us see how this work. In this example we have added public in all the
Person attributes. By default all attributes are always public but for
readability it is good practice to add it.
public greetings(){
return name + ' ' + age
}
}
Public
Protected
Private
Static
Further we can use readonly which will prevents assignments to the field
outside of the constructor.
Let us take another example to further understand this concept.
// class person
class Person {
private readonly credentials: string = ''
private name: string = ''
private department: string = ''
constructor(value: string){
this.credentials = value
}
public getName(){
return `Employee name: ${this.name}, Department: $
{this.department}`
}
}
One more thing we should not is all our attributes are private and cannot
be accessed outside the class object. Take note that we can only access
public objects outside the class object.
Try this
Inheritance.
Relationships and subclasses between objects can be assigned, allowing
developers to reuse a common logic while still maintaining a unique
hierarchy. This property of OOP forces a more thorough data analysis,
reduces development time and ensures a higher level of accuracy.
Take a look at this example. You will NOTE that by extending the
TeslaCompnany we have inherited all the public attributes and can call
them when we create a new instance of TeslaEmployee. This can allow us
have a base class and reuse the base class in different subsclasses.
// class company
type Department = {
name: string
}
type Employee = {
name: string
age: number
}
class TeslaCompany {
private static role = "Admin"
private readonly credentials: string = ''
private departments: (Department)[] = []
private employees: (Employee)[] = []
constructor(cred: string) {
this.credentials = cred
}
addDepartment(value: Department) {
this.departments = [...this.departments, value]
}
addEmployee(value: Employee) {
this.employees = [...this.employees, value]
}
}
}
}
Polymorphism.
Objects can take on more than one form depending on the context. The
program will determine which meaning or usage is necessary for each
execution of that object, cutting down the need to duplicate code.
// class Person
class Person {
public name: string = ''
public role: string = ''
}
Functions
Function Parameters
The types of function parameters work similarly to variable
declarations. If the parameter has a default value, it will take on
the type of that value. Otherwise, we may declare the type of that
parameter by adding a type annotation after its name.
logAgeAndName('Mulan', 16);
// Argument of type 'string' is not assignable to parameter of type 'number'
Optional Parameters
Function parameters can be made optional by adding a ? question
mark after their name, before any type annotation. TypeScript will
understand that they don’t need to be provided when the function
is invoked. Additionally, their type is a union that
includes undefined. This means that if a given function does not use
the optional parameter, its value is set to undefined.
if (reason) {
console.log(`Because: ${reason}!`);
}
}
logFavoriteNumberAndReason();
// Error: Expected 1-2 arguments, but got 0.
Return Types
Most functions are written in a way that TypeScript can infer what
value they return by looking at all the return statements in the
function. Functions that don’t return a value are considered to
have a return type of void.
Function Types
Function types may be represented in the type system. They look
a lot like an arrow lambda, but with the return type instead of the
function body.
withIncrementedValue((receivedValue) => {
console.log('Got', receivedValue);
});
Generics
Contribute to Docs
Syntax
Generics are defined with < > brackets surrounding name(s) of
the generic type(s), like Array<T> or Map<Key, Value>.
interface MyType<GenericValue> {
value: GenericValue;
}
Classes
Functions
Interfaces
Type aliases
interface Box<Value> {
value: Value;
}
Interfaces
Contribute to Docs
Syntax
Interfaces may be declared by:
interface myType {
memberOne: string;
memberTwo: number;
};
let myVar: myType = {"My favorite number is ", 42 };
interface Dog {
fluffy: boolean;
woof(): string;
}
if (dog.fluffy) {
console.log('What a floof!');
}
dog.bark();
// Error: Property 'bark' does not exist on type 'Dog'.
}
Optional Members
Here, the Pet interface uses ? to set name as an optional member.
The only member that is required is species. Declaring an object of
type Pet doesn’t need a name but does need a species:
interface Pet {
name?: string;
species: string;
}
Interface Extensions
Interfaces may be marked as extending another interface. Doing
so indicates that the derived child interface (the interface
extending others) includes all members from the base parent
interfaces (the interface being extended).
interface Animal {
walk(): void;
}
if (cat.fluffy) {
// Ok: defined on Cat
console.log('Floof!!');
cat.purr(); // Ok: defined on Cat
}
animal.purr();
// Error: Property 'purr' does not exist on type 'Animal'.
}
Iterables
An object is deemed iterable if it has an implementation for the Symbol.iterator property.
Some built-in types like Array, Map, Set, String, Int32Array, Uint32Array, etc. have
their Symbol.iterator property already implemented. Symbol.iterator function on an object is
responsible for returning the list of values to iterate on.
Iterable interface
Iterable is a type we can use if we want to take in types listed above which are iterable. Here
is an example:
for..of statements
for..ofloops over an iterable object, invoking the Symbol.iterator property on the object.
Here is a simple for..of loop on an array:
let someArray = [1, "string", false];
for (let entry of someArray) {
console.log(entry); // 1, "string", false
}
Both for..of and for..in statements iterate over lists; the values iterated on are different
though, for..in returns a list of keys on the object being iterated, whereas for..of returns a list
of values of the numeric properties of the object being iterated.
Another distinction is that for..in operates on any object; it serves as a way to inspect
properties on this object. for..of on the other hand, is mainly interested in values of iterable
objects. Built-in objects like Map and Set implement Symbol.iterator property allowing
access to stored values.
Code generation
When targeting an ES5 or ES3-compliant engine, iterators are only allowed on values
of Array type. It is an error to use for..of loops on non-Array values, even if these non-Array
values implement the Symbol.iterator property.
The compiler will generate a simple for loop for a for..of loop, for instance:
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}
Enums
Enums are one of the few features TypeScript has which is not a type-level extension of
JavaScript.
Enums allow a developer to define a set of named constants. Using enums can make it easier
to document intent, or create a set of distinct cases. TypeScript provides both numeric and
string-based enums.
Numeric enums
We’ll first start off with numeric enums, which are probably more familiar if you’re coming
from other languages. An enum can be defined using the enum keyword.
enum Direction {
Up = 1,
Down,
Left,
Right,
}
Try
Above, we have a numeric enum where Up is initialized with 1. All of the following members
are auto-incremented from that point on. In other words, Direction.Up has the
value 1, Down has 2, Left has 3, and Right has 4.
enum Direction {
Up,
Down,
Left,
Right,
}
Try
Here, Up would have the value 0, Down would have 1, etc. This auto-incrementing behavior
is useful for cases where we might not care about the member values themselves, but do care
that each value is distinct from other values in the same enum.
Using an enum is simple: just access any member as a property off of the enum itself, and
declare types using the name of the enum:
enum UserResponse {
No = 0,
Yes = 1,
}
Numeric enums can be mixed in computed and constant members (see below). The short
story is, enums without initializers either need to be first, or have to come after numeric
enums initialized with numeric constants or other constant enum members. In other words,
the following isn’t allowed:
enum E {
A = getSomeValue(),
B,
Enum member must have initializer.Enum member must have
initializer.
}
Try
String enums
String enums are a similar concept, but have some subtle runtime differences as documented
below. In a string enum, each member has to be constant-initialized with a string literal, or
with another string enum member.
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
Try
While string enums don’t have auto-incrementing behavior, string enums have the benefit
that they “serialize” well. In other words, if you were debugging and had to read the runtime
value of a numeric enum, the value is often opaque - it doesn’t convey any useful meaning on
its own (though reverse mapping can often help). String enums allow you to give a
meaningful and readable value when your code runs, independent of the name of the enum
member itself.
Heterogeneous enums
Technically enums can be mixed with string and numeric members, but it’s not clear why you
would ever want to do so:
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
Try
Unless you’re really trying to take advantage of JavaScript’s runtime behavior in a clever
way, it’s advised that you don’t do this.
It is the first member in the enum and it has no initializer, in which case it’s assigned
the value 0:
// E.X is constant:
enum E {
X,
}
Try
It does not have an initializer and the preceding enum member was
a numeric constant. In this case the value of the current enum member will be the
value of the preceding enum member plus one.
// All enum members in 'E1' and 'E2' are constant.
enum E1 {
X,
Y,
Z,
}
enum E2 {
A = 1,
B,
C,
}
Try
The enum member is initialized with a constant enum expression. A constant enum
expression is a subset of TypeScript expressions that can be fully evaluated at
compile time. An expression is a constant enum expression if it is:
1. a literal enum expression (basically a string literal or a numeric literal)
2. a reference to previously defined constant enum member (which can
originate from a different enum)
3. a parenthesized constant enum expression
4. one of the +, -, ~ unary operators applied to constant enum expression
5. +, -, *, /, %, <<, >>, >>>, &, |, ^ binary operators with constant enum
expressions as operands
enum FileAccess {
// constant members
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// computed member
G = "123".length,
}
Try
When all members in an enum have literal enum values, some special semantics come into
play.
The first is that enum members also become types as well! For example, we can say that
certain members can only have the value of an enum member:
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
Type 'ShapeKind.Square' is not assignable to type
'ShapeKind.Circle'.Type 'ShapeKind.Square' is not assignable to type
'ShapeKind.Circle'.
radius: 100,
};
Try
The other change is that enum types themselves effectively become a union of each enum
member. With union enums, the type system is able to leverage the fact that it knows the
exact set of values that exist in the enum itself. Because of that, TypeScript can catch bugs
where we might be comparing values incorrectly. For example:
enum E {
Foo,
Bar,
}
function f(x: E) {
if (x !== E.Foo || x !== E.Bar) {
This comparison appears to be unintentional because the types 'E.Foo'
and 'E.Bar' have no overlap.This comparison appears to be
unintentional because the types 'E.Foo' and 'E.Bar' have no overlap.
//
}
}
Try
In that example, we first checked whether x was not E.Foo. If that check succeeds, then
our || will short-circuit, and the body of the ‘if’ will run. However, if the check didn’t
succeed, then x can only be E.Foo, so it doesn’t make sense to see whether it’s not equal
to E.Bar.
Enums at runtime
Enums are real objects that exist at runtime. For example, the following enum
enum E {
X,
Y,
Z,
}
Try
enum E {
X,
Y,
Z,
}
enum LogLevel {
ERROR,
WARN,
INFO,
DEBUG,
}
/**
* This is equivalent to:
* type LogLevelStrings = 'ERROR' | 'WARN' | 'INFO' | 'DEBUG';
*/
type LogLevelStrings = keyof typeof LogLevel;
Reverse mappings
In addition to creating an object with property names for members, numeric enums members
also get a reverse mapping from enum values to enum names. For example, in this example:
enum Enum {
A,
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
Try
Try
In this generated code, an enum is compiled into an object that stores both forward (name -
> value) and reverse (value -> name) mappings. References to other enum members are
always emitted as property accesses and never inlined.
Keep in mind that string enum members do not get a reverse mapping generated at all.
const enums
In most cases, enums are a perfectly valid solution. However sometimes requirements are
tighter. To avoid paying the cost of extra generated code and additional indirection when
accessing enum values, it’s possible to use const enums. Const enums are defined using
the const modifier on our enums:
Const enums can only use constant enum expressions and unlike regular enums they are
completely removed during compilation. Const enum members are inlined at use sites. This is
possible since const enums cannot have computed members.
Decorators
A Decorator is a special kind of declaration that can be attached
to a class declaration, method, accessor, property, or parameter.
Decorators use the form @expression, where expression must evaluate
to a function that will be called at runtime with information about
the decorated declaration.
Decorator Factories
If we want to customize how a decorator is applied to a
declaration, we can write a decorator factory. A Decorator
Factory is simply a function that returns the expression that will
be called by the decorator at runtime.
Decorator Composition
Multiple decorators can be applied to a declaration, for example
on a single line:
@f @g x
Try
On multiple lines:
@f
@g
x
Try
function first() {
console.log("first(): factory evaluated");
return function (target: any, propertyKey: string, descriptor:
PropertyDescriptor) {
console.log("first(): called");
};
}
function second() {
console.log("second(): factory evaluated");
return function (target: any, propertyKey: string, descriptor:
PropertyDescriptor) {
console.log("second(): called");
};
}
class ExampleClass {
@first()
@second()
method() {}
}
Try
Decorator Evaluation
There is a well defined order to how decorators applied to various
declarations inside of a class are applied:
1. Parameter Decorators, followed by Method, Accessor,
or Property Decorators are applied for each instance
member.
2. Parameter Decorators, followed by Method, Accessor,
or Property Decorators are applied for each static member.
3. Parameter Decorators are applied for the constructor.
4. Class Decorators are applied for the class.
Class Decorators
A Class Decorator is declared just before a class declaration. The
class decorator is applied to the constructor of the class and can
be used to observe, modify, or replace a class definition. A class
decorator cannot be used in a declaration file, or in any other
ambient context (such as on a declare class).
NOTE Should you choose to return a new constructor function, you must
take care to maintain the original prototype. The logic that applies
decorators at runtime will not do this for you.
@sealed
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
Try
When @sealed is executed, it will seal both the constructor and its
prototype, and will therefore prevent any further functionality
from being added to or removed from this class during runtime by
accessing BugReport.prototype or by defining properties
on BugReport itself (note that ES2015 classes are really just
syntactic sugar to prototype-based constructor functions). This
decorator does not prevent classes from sub-classing BugReport.
@reportableClassDecorator
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
}
// Note that the decorator _does not_ change the TypeScript type
// and so the new property `reportingURL` is not known
// to the type system:
bug.reportingURL;
Property 'reportingURL' does not exist on type 'BugReport'.Property
'reportingURL' does not exist on type 'BugReport'. Try
Method Decorators
A Method Decorator is declared just before a method declaration.
The decorator is applied to the Property Descriptor for the
method, and can be used to observe, modify, or replace a method
definition. A method decorator cannot be used in a declaration
file, on an overload, or in any other ambient context (such as in
a declare class).
The NOTE Property Descriptor will be undefined if your script target is less
than ES5.
The return value is ignored if your script target is less than NOTE ES5.
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
Try
Accessor Decorators
An Accessor Decorator is declared just before an accessor
declaration. The accessor decorator is applied to the Property
Descriptor for the accessor and can be used to observe, modify,
or replace an accessor’s definitions. An accessor decorator cannot
be used in a declaration file, or in any other ambient context
(such as in a declare class).
TypeScript disallows decorating both the NOTE get and set accessor for a
single member. Instead, all decorators for the member must be applied to
the first accessor specified in document order. This is because decorators
apply to a Property Descriptor, which combines both
the get and set accessor, not each declaration separately.
The NOTE Property Descriptor will be undefined if your script target is less
than ES5.
The return value is ignor NOTEed if your script target is less than ES5.
The following is an example of an accessor decorator ( @configurable)
applied to a member of the Point class:
class Point {
private _x: number;
private _y: number;
constructor(x: number, y: number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
Try
Property Decorators
A Property Decorator is declared just before a property
declaration. A property decorator cannot be used in a declaration
file, or in any other ambient context (such as in a declare class).
class Greeter {
@format("Hello, %s")
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
let formatString = getFormat(this, "greeting");
return formatString.replace("%s", this.greeting);
}
}
import "reflect-metadata";
const formatMetadataKey = Symbol("format");
function format(formatString: string) {
return Reflect.metadata(formatMetadataKey, formatString);
}
function getFormat(target: any, propertyKey: string) {
return Reflect.getMetadata(formatMetadataKey, target, propertyKey);
}
Parameter Decorators
A Parameter Decorator is declared just before a parameter
declaration. The parameter decorator is applied to the function for
a class constructor or method declaration. A parameter decorator
cannot be used in a declaration file, an overload, or in any other
ambient context (such as in a declare class).
class BugReport {
type = "report";
title: string;
constructor(t: string) {
this.title = t;
}
@validate
print(@required verbose: boolean) {
if (verbose) {
return `type: ${this.type}\ntitle: ${this.title}`;
} else {
return this.title;
}
}
}
Try
We can then define the @required and @validate decorators using the
following function declarations:
import "reflect-metadata";
const requiredMetadataKey = Symbol("required");
descriptor.value = function () {
let requiredParameters: number[] =
Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName);
if (requiredParameters) {
for (let parameterIndex of requiredParameters) {
if (parameterIndex >= arguments.length ||
arguments[parameterIndex] === undefined) {
throw new Error("Missing required argument.");
}
}
}
return method.apply(this, arguments);
};
}
Try
This example requires the NOTE reflect-metadata library. See Metadata for
more information about the reflect-metadata library.