Udemy: This course about TypeScript, including features like decorators and advanced types. By Insturctor Maximilian Schwarzmüller
enum Role {ADMIN, READ_ONLY, AUTHOR };
const person = {
name: 'Meyada',
age: 26,
hobbies: ['Sports', 'Coding'],
role: Role.ADMIN
};
let favoriteAc: string[];
favoriteAc = ['Coding'];
console.log(person.name);
for(const hobby of person.hobbies) {
console.log(hobby.toUpperCase());
}
if (person.role === Role.AUTHOR) {
console.log('is author');
This code makes use of TypeScript's enumeration feature to define a set of named constants (ADMIN, READ_ONLY, AUTHOR) that are used to set the value of the role property in the person object. This provides a more readable and maintainable way of setting and comparing the role, rather than using raw string or numeric values. Additionally, the code makes use of the for-of loop and the TypeScript's string array type to log the person's hobbies in uppercase.
type Combinable = number | string;
type ConversionDescriptor = 'as-number' | 'as-text';
function combine(
input1: Combinable,
input2: Combinable,
resultConversion: ConversionDescriptor
){
let result;
if (typeof input1 === 'number' && typeof input2 === 'number' || resultConversion === 'as-number' ){
result = +input1 + +input2;
} else {
result = input1.toString() + input2.toString();
}
return result;
}
const combineAges = combine(26, 40, 'as-number');
console.log(combineAges);
const combineStringAges = combine('26', '40', 'as-number');
console.log(combineStringAges);
const combineName = combine('Meya', 'Anne', 'as-text')
console.log(combineName);
This TypeScript code defines a function
combine
that takes three arguments:input1
andinput2
of type Combinable, which is a union type ofnumber
orstring
, andresultConversion
of typeConversionDescriptor
, which is a string literal type with two possible values: 'as-number' or 'as-text'. The function checks the type ofinput1
andinput2
and the value ofresultConversion
to determine how to combine the inputs. If both inputs are numbers orresultConversion
is 'as-number', it performs mathematical addition on the inputs. Otherwise, it concatenates the inputs as strings. The code also includes examples of how the function can be used with different inputs and conversion types.
-
"target": "es6",
This option sets the JavaScript language version for emitted JavaScript, meaning that the code generated by the compiler will be compatible with the specified version of JavaScript. This allows developers to use newer features of the language and have them transpiled for backwards compatibility. -
"lib": [ "dom", "es6", "dom.iterable", "scripthost" ],
This option specifies a set of bundled library declaration files that describe the target runtime environment. This allows developers to use libraries and APIs that are built-in to the specified environment, without having to include them in their project. -
"module": "commonjs",
This option specify what module code is generated, and the developer can use commonjs, es2015, esnext and so on. This allows developers to use the module system that is most appropriate for their project. -
"rootDir": "./src",
This option specifies the root folder within your source files. This allows developers to specify the root directory of their source files, making it easier to organize and manage the project. -
"removeComments": true
This option disables emitting comments. This can be useful for reducing the size of the generated JavaScript files or for obscuring the source code. -
"moduleSuffixes": [],
This option allows developers to specify additional file types that should be considered when resolving modules, such as custom file extensions. -
"resolveJsonModule": true,
This option allows developers to import .json files. It can be useful when loading data or configuration files in JavaScript. -
"noResolve": true,
This option helps developers to prevent 'import's, 'require's or'<reference>'
s from expanding the number of files TypeScript should add to a project. It can be useful when you want to limit the number of files that are included in a project. -
"allowJs": true,
This option allows developers to include JavaScript files in their TypeScript projects. This can be useful when working with a codebase that contains both TypeScript and JavaScript files. -
"checkJs": true,
This option allows developers to enable error reporting in type-checked JavaScript files. This can be useful when working with a codebase that contains both TypeScript and JavaScript files and you want to ensure that the JavaScript files are type-safe. -
"maxNodeModuleJsDepth": 1,
This option allows developers to specify the maximum folder depth used for checking JavaScript files from 'node_modules'. This can be useful when working with a large codebase and you want to limit the number of files that are checked for errors. -
"declaration": true,
This option allows developers to generate .d.ts files from TypeScript and JavaScript files in their project. This can be useful when working with other libraries or when you want to make your code more self-documenting. -
"sourceMap": true,
This option allows developers to create source map files for emitted JavaScript files. This can be useful when debugging the generated JavaScript code, as it allows developers to see the original TypeScript code that generated the JavaScript. -
"declarationMap": true,
This option allows developers to create sourcemaps for d.ts files. This can be useful when debugging the generated declaration files, as it allows developers to see the original TypeScript code that generated the declarations. -
emitDeclarationOnly": true,
This option allows developers to only output d.ts files and not JavaScript files. This can be useful when working with other libraries and you only need the declarations for type checking. -
"outFile": "./",
This option allows developers to specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. This can be useful when you want to bundle multiple JavaScript files into a single file for deployment. -
"noEmit": true,
This option allows developers to disable emitting files from a compilation. This can be useful when you want to perform a type check of the code without generating any output files. -
"importHelpers": true
This option allows developers to import helper functions from tslib once per project, instead of including them per-file. This can be useful when you want to reduce the number of files that are included in a project. -
"downlevelIteration": true,
This option allows developers to emit more compliant, but verbose and less performant JavaScript for iteration. This can be useful when you want to ensure that the generated code is compatible with older JavaScript engines. -
"jsx": "preserve",
This option allows developers to specify how JSX code is generated by the compiler. The default value is "preserve", which means that the JSX code will be included in the output without modification. This can be useful when working with React and you want to use JSX syntax in your TypeScript code. -
"jsxFactory": "",
This option allows developers to specify the JSX factory function that should be used when targeting React JSX emit. This can be useful when working with React and you want to specify a custom factory function for creating React elements. -
"jsxFragmentFactory": "",
This option allows developers to specify the JSX Fragment reference that should be used for fragments when targeting React JSX emit. This can be useful when working with React and you want to specify a custom Fragment type. -
"jsxImportSource": "",
This option allows developers to specify the module specifier that should be used to import the JSX factory functions when using 'jsx: react-jsx*'. This can be useful when working with React and you want to specify a custom import path for the JSX factory functions. -
"reactNamespace": "",
This option allows developers to specify the object invoked for 'createElement'. This can be useful when working with React and you want to specify a custom namespace for the React functions.
-
"Public"
members are accessible from anywhere, both within the class and outside of it. This is the default access level for class members if no access modifier is specified. -
"Private"
members, on the other hand, are only accessible within the class in which they are defined. They cannot be accessed or modified from outside the class.
class MyClass {
public myPublicField = 'I am public';
private myPrivateField = 'I am private';
}
let myObject = new MyClass();
console.log(myObject.myPublicField); // Output: "I am public"
console.log(myObject.myPrivateField); // Error: Property 'myPrivateField' is private and only accessible within class 'MyClass'.
"readonly"
access modifier is used to create properties that can only be read and cannot be modified. Once a readonly property is set, its value cannot be changed.
class Department {
// private id: string;
// private name: string;
private employees: string[] = [];
constructor(private readonly id: string, public name: string ) {
// this.id = id;
// this.name = n;
}
}
- In TypeScript, the
"protected"
access modifier is similar to the"private"
access modifier, but with one key difference: protected members can also be accessed from within derived classes.
When it comes to overriding properties, the "protected"
access modifier can be useful because it allows derived classes to access and modify the value of the base class property.
class Department {
// private id: string;
// private name: string;
protected employees: string[] = [];
constructor(private id: string, public name: string ) {
// this.id = id;
// this.name = n;
}
describe(this: Department) {
console.log(`Department (${this.id}: ${this.name})`);
}
addEmployee(employee: string){
// Validation etc
this.employees.push(employee);
}
printEmployeeInformattion(){
console.log(this.employees.length);
console.log(this.employees);
}
}
class ITDepartment extends Department {
admins: string[];
constructor(id: string, admins: string[]){
super(id, 'IT');
this.admins = admins;
}
}
class AccountingDepartment extends Department {
constructor(id: string, private reports: string[]){
super(id, 'Accounting');
}
addEmployee(name: string) {
if (name === 'Meya') {
return;
}
this.employees.push(name);
}
addReport(text: string) {
this.reports.push(text);
}
printReports() {
console.log(this.reports);
}
}
const it = new ITDepartment('D1', ['Meya']);
it.addEmployee('Meya');
it.addEmployee('Malee');
// it.employees[2] = 'Mario';
it.describe();
it.name = 'New name';
it.printEmployeeInformattion();
console.log(it);
const accounting = new AccountingDepartment('D2', []);
accounting.addReport('Something went wrong...');
accounting.addEmployee('Meya');
accounting.addEmployee('Malee');
accounting.printReports();
accounting.printEmployeeInformattion();
- In TypeScript, getters and setters are used to control access to the properties of an object. A getter is a method that is used to retrieve the value of a property, while a setter is a method that is used to set the value of a property.
class MyClass {
private _myProperty: string;
get myProperty(): string {
return this._myProperty;
}
set myProperty(value: string) {
this._myProperty = value;
}
}
let myObject = new MyClass();
myObject.myProperty = 'new value';
console.log(myObject.myProperty); // Output: "new value"
In this example, the
"myProperty"
property is defined as private, and can only be accessed and modified using the public getter and setter methods. Getters and setters are useful in situations where you want to control access to a property, or perform additional logic when a property is accessed or modified. For example, you can use a setter to validate the value being set before it is stored, or use a getter to return a computed value based on the current state of the object. Also, with the latest version of TypeScript, you can use the getter and setter syntax directly on the property, it's called the"Accessor Property"
abstract class Shape {
abstract getArea(): number;
}
class Square extends Shape {
side: number;
constructor(side: number) {
super();
this.side = side;
}
getArea(): number {
return this.side * this.side;
}
}
let square = new Square(5);
console.log(square.getArea()); // Output: 25
An abstract class in TypeScript is a class that cannot be instantiated, but can be extended by other classes. It serves as a base class for other classes and provides a common interface for derived classes. Abstract classes are defined using the
"abstract"
keyword and can contain both abstract and non-abstract members (methods and properties). An abstract class must be extended by a derived class, which must implement all the abstract methods and properties defined in the base class. It's important to note that you can't create an instance of an abstract class and can't override a non-abstract method with an abstract one.
class Singleton {
private static instance: Singleton;
private constructor() {}
static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}
}
let singleton1 = Singleton.getInstance();
let singleton2 = Singleton.getInstance();
console.log(singleton1 === singleton2); // Output: true
A singleton is a design pattern that ensures that a class has only one instance and provides a global point of access to that instance. It can be implemented in TypeScript using a private constructor and a private static instance variable. The private constructor prevents other classes from instantiating the class directly, and the private static instance variable holds the single instance of the class, which can be accessed using a public static method
"getInstance"
. The use of private constructors can also prevent other classes from instantiating an object of the class, allowing the developer to control the way objects are created, and to ensure that a class has only one instance, like in the singleton pattern.
interface Person {
firstName: string;
lastName: string;
sayHello(): string;
}
class Student implements Person {
firstName: string;
lastName: string;
sayHello(): string {
return `Hello, my name is ${this.firstName} ${this.lastName}.`;
}
}
the interface
"Person"
defines the properties"firstName"
and"lastName"
as well as the method"sayHello"
. The class"Student"
implements this interface, meaning that it must have the properties and methods defined in the interface. The class must provide an implementation for the sayHello method, which is defined in the interface. Using interfaces with classes provides a way to ensure that a class has the properties and methods that it should have, without providing an implementation for them. This can be useful in situations where you want to ensure that a class conforms to a certain structure, or where you want to create a contract for the shape of an object that multiple classes can implement.
interface Point {
readonly x: number;
readonly y: number;
}
let point: Point = { x: 10, y: 20 };
point.x = 30; // Error: Cannot assign to 'x' because it is a read-only property.
In TypeScript, readonly interface properties are properties in an interface that can only be read, not written to. This is achieved by using the
readonly
keyword before the property name. The value of a readonly property must be set at the time of object creation and cannot be changed
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
type Shape = Square | Rectangle;
function area(s: Shape) {
switch (s.kind) {
case "square":
return s.size * s.size;
case "rectangle":
return s.width * s.height;
}
}
In TypeScript, a discriminated union is a type that represents a value that can be one of several types. The union is "discriminated" because a property that distinguishes between the different types of the union is used to determine which type the value is. This allows the type system to infer the type of the value, and provide more specific type information when working with the value.
let value: any = "Hello, world!";
let str = value as string;
In TypeScript, type casting is a way to tell the compiler to treat a value as a specific type, even if its original type is different. This can be useful when working with APIs that return values of a more general type than the actual type of the values, or when working with values that are inferred as a more general type than their actual type.
function formatName(first: string, last: string): string;
function formatName(first: string): string;
function formatName(first: string, last?: string): string {
if (last) {
return `${first} ${last}`;
} else {
return first;
}
}
let fullName = formatName("John", "Doe");
let firstName = formatName("Jane");
In TypeScript, function overloading allows you to declare multiple functions with the same name but different parameter lists. This can be useful when you want to provide different implementations for a function based on the types of its arguments. When calling a function with overloaded signatures, TypeScript will choose the best match based on the types of the arguments being passed.
let user = { name: "John", address: { city: "London" } };
let city = user?.address?.city;
console.log(city); // Output: "London"
user = null;
city = user?.address?.city;
console.log(city); // Output: undefined
Optional chaining is a feature in TypeScript (and in many other programming languages) that allows you to access properties or methods of an object without having to check if the object is
null
orundefined
first.
The optional chaining operator
?
. allows you to safely access properties or methods of an object without having to check if the object isnull
orundefined
. If the object isnull
orundefined
, the expression short-circuits and returnsundefined
instead of throwing an error.
interface HasName {
name: string;
}
function printName<T extends HasName>(obj: T): void {
console.log(obj.name);
}
let person = { name: "John", age: 30 };
printName(person);
let animal = { species: "Dog" };
In TypeScript, constraints are used to restrict the types of values that can be used with a generic type. When working with generic types, you can use constraints to ensure that the type of value being used meets certain requirements.
Decorator in TypeScript is a special syntax that can be used to modify the behavior of a class, method, property, or parameter. Decorators can be used to add metadata to your code, to change the behavior of a class or its members, or to implement aspect-oriented programming (AOP) techniques.
function logMethod(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function(...args: any[]) {
console.log(`Calling method ${propertyKey} with arguments`, args);
const result = originalMethod.apply(this, args);
console.log(`Method ${propertyKey} returned`, result);
return result;
};
}
class UserService {
@logMethod
getUser(id: number) {
return { id: id, name: "John" };
}
}
const userService = new UserService();
userService.getUser(1);
// Output:
// Calling method getUser with arguments [ 1 ]
// Method getUser returned Object { id: 1, name: "John" }
The
logMethod
decorator is used to log information about a method before and after it is called. ThelogMethod
decorator takes three arguments:target
,propertyKey
, anddescriptor
. Thetarget
argument is the instance of the class that the method belongs to, thepropertyKey
argument is the name of the method, and thedescriptor
argument is a property descriptor that can be used to modify the behavior of the method.
In the implementation of the
logMethod
decorator, we use thedescriptor.value
property to get a reference to the original method, and then we use thedescriptor.value
property to assign a new function that wraps the original method and logs information about it before and after it is called.
A property decorator in TypeScript is a special syntax that can be used to modify the behavior of a class property. Property decorators can be used to add metadata to your code, to change the behavior of a property, or to implement aspect-oriented programming (AOP) techniques.
function logProperty(target: any, propertyKey: string) {
let _val = target[propertyKey];
const getter = function() {
console.log(`Getting value for property ${propertyKey}: ${_val}`);
return _val;
};
const setter = function(newVal: any) {
console.log(`Setting value for property ${propertyKey}: ${newVal}`);
_val = newVal;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
}
class User {
@logProperty
name: string;
}
const user = new User();
user.name = "John";
console.log(user.name);
// Output:
// Setting value for property name: John
// Getting value for property name: John
In this example, the
logProperty
decorator is used to log information about a property whenever its value is set or retrieved. ThelogProperty
decorator takes two arguments:target
andpropertyKey
. Thetarget
argument is the instance of the class that the property belongs to, and thepropertyKey
argument is the name of the property.
In the implementation of the
logProperty
decorator, we use theObject.defineProperty
method to define a new property with a getter and a setter that log information about the property whenever its value is set or retrieved.
In TypeScript, class decorators can return a new class that will replace the original one. This is useful when you want to change the behavior or properties of a class.
function logClass(target: any) {
console.log(`Creating instance of class ${target.name}`);
return class extends target {
constructor(...args: any[]) {
console.log(`Calling constructor of class ${target.name}`);
super(...args);
}
};
}
@logClass
class User {
constructor(public name: string) {}
}
const user = new User("John");
console.log(user);
// Output:
// Creating instance of class User
// Calling constructor of class User
// User { name: "John" }
The
logClass
decorator logs information about the creation of an instance of theUser
class and the call to its constructor. The decorator returns a new class that extends the originalUser
class and adds logging to the constructor.
The Autobind decorator is a TypeScript decorator that can be used to automatically bind the
this
keyword to the correct value when calling a method. This is useful when you want to ensure that the method always refers to the instance of the class, even when the method is passed as a callback or used in a different context.
function autobind(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjustedDescriptor: PropertyDescriptor = {
configurable: true,
get() {
const boundFn = originalMethod.bind(this);
return boundFn;
}
};
return adjustedDescriptor;
}
class User {
name = "John";
@autobind
getName() {
return this.name;
}
}
const user = new User();
const getName = user.getName;
console.log(getName());
// Output:
// John
The Autobind decorator is used to bind the
this
keyword to the correct value when calling thegetName
method. This means that the method can be called as a standalone function, and it will still refer to theUser
instance, not the global context.
function required(target: any, propertyKey: string) {
Object.defineProperty(target, propertyKey, {
get() {
return this["_" + propertyKey];
},
set(value) {
if (!value) {
throw new Error(`${propertyKey} is required.`);
}
this["_" + propertyKey] = value;
},
});
}
class User {
@required
name!: string;
@required
email!: string;
}
const user = new User();
user.name = "John";
user.email = "john@example.com";
console.log(user);
// Output:
// User { _name: "John", _email: "john@example.com" }
The
required
decorator sets a getter and setter on the decorated properties that validate the presence of a value. When the value of a decorated property is set toundefined
ornull
, the setter throws an error indicating that the property is required
class Form {
private formElement: HTMLFormElement;
constructor(selector: string) {
this.formElement = document.querySelector(selector);
if (!this.formElement) {
throw new Error(`Cannot find form with selector "${selector}"`);
}
this.render();
}
private render() {
this.formElement.innerHTML = `
<input type="text" placeholder="Username" />
<input type="password" placeholder="Password" />
<button>Sign In</button>
`;
}
}
const form = new Form("#sign-in-form");
console.log(form);
The
Form
class is used to select a form element from the DOM using a selector, and render the form content using OOP concepts. Therender
method is used to render the content of the form, and the class constructor is used to select the form element and render the form content. This approach can be extended to handle more complex rendering scenarios and allow for easy maintenance and reuse of code.
class Form {
private formElement: HTMLFormElement;
private submitButton: HTMLButtonElement;
constructor(selector: string) {
this.formElement = document.querySelector(selector);
if (!this.formElement) {
throw new Error(`Cannot find form with selector "${selector}"`);
}
this.submitButton = this.formElement.querySelector("button");
this.submitButton.addEventListener("click", this.submitForm.bind(this));
this.render();
}
private render() {
this.formElement.innerHTML = `
<input type="text" placeholder="Username" />
<input type="password" placeholder="Password" />
<button>Sign In</button>
`;
}
private submitForm() {
console.log("Form submitted!");
}
}
const form = new Form("#sign-in-form");
console.log(form);
The
Form
class is used to select a form element from the DOM using a selector, and render the form content using OOP concepts. Therender
method is used to render the content of the form, and the class constructor is used to select the form element, the submit button, and add a click event listener to the submit button. When the submit button is clicked, thesubmitForm
method will be called, which will log a message to the console. This approach can be extended to handle more complex interaction scenarios and allow for easy maintenance and reuse of code.
class ProjectItem {
constructor(public id: string, public title: string, public description: string) {}
render() {
const itemEl = document.createElement("li");
itemEl.innerHTML = `
<h2>${this.title}</h2>
<p>${this.description}</p>
`;
itemEl.id = this.id;
return itemEl;
}
}
class ProjectList {
private projects: ProjectItem[] = [];
constructor(private type: "active" | "finished") {
const prjItems = document.querySelectorAll(`#${type}-projects li`);
for (const prjItem of prjItems) {
const id = prjItem.id;
const title = prjItem.querySelector("h2")!.textContent!;
const description = prjItem.querySelector("p")!.textContent!;
this.projects.push(new ProjectItem(id, title, description));
}
this.renderProjects();
}
private renderProjects() {
const listEl = document.querySelector(`#${this.type}-projects ul`)!;
listEl.innerHTML = "";
for (const prjItem of this.projects) {
const listItem = prjItem.render();
listEl.appendChild(listItem);
}
}
}
const activeProjectList = new ProjectList("active");
const finishedProjectList = new ProjectList("finished");
The
ProjectItem
class is used to represent each item in the project list, and theProjectList
class is used to manage the list of project items. TheProjectItem
class has a render method that creates a new DOM element for each project item. TheProjectList
class has a constructor that selects the project items from the DOM and creates a newProjectItem
instance for each item, and arenderProjects
method that renders the list of project items on the screen. This approach can be extended to handle more complex rendering scenarios and allow for easy maintenance and reuse of code.
interface Draggable {
dragStartHandler(event: DragEvent): void;
dragEndHandler(event: DragEvent): void;
}
class Project implements Draggable {
constructor(public id: string, public title: string, public description: string, public people: number) {}
dragStartHandler(event: DragEvent) {
event.dataTransfer!.setData("text/plain", this.id);
event.dataTransfer!.effectAllowed = "move";
}
dragEndHandler(_: DragEvent) {
console.log("DragEnd");
}
}
class ProjectList {
private projects: Project[] = [];
constructor(private type: "active" | "finished") {
const prjItems = document.querySelectorAll(`#${type}-projects li`);
for (const prjItem of prjItems) {
const id = prjItem.id;
const title = prjItem.querySelector("h2")!.textContent!;
const description = prjItem.querySelector("p")!.textContent!;
const people = +prjItem.querySelector("span")!.textContent!;
const project = new Project(id, title, description, people);
prjItem.addEventListener("dragstart", project.dragStartHandler.bind(project));
prjItem.addEventListener("dragend", project.dragEndHandler);
this.projects.push(project);
}
}
}
new ProjectList("active");
new ProjectList("finished");
The
Draggable
interface defines thedragStartHandler
anddragEndHandler
methods that a draggable object should have. TheProject
class implements this interface, and provides its own implementation of the methods. TheProjectList
class creates a newProject
instance for each project item, and adds event listeners for thedragstart
anddragend
events to handle the drag and drop behavior. By using interfaces in this way, you can ensure that only objects that have the necessary properties and methods can be dragged.
In TypeScript, namespaces provide a way to organize your code into logical groups, and avoid naming conflicts with other code. A namespace is simply a named scope that can contain functions, classes, interfaces, and other objects.
namespace MathUtils {
export function add(a: number, b: number): number {
return a + b;
}
export function subtract(a: number, b: number): number {
return a - b;
}
}
const result1 = MathUtils.add(10, 5); // returns 15
const result2 = MathUtils.subtract(10, 5); // returns 5
In this example, the
MathUtils
namespace contains two functions:add
andsubtract
. These functions are exported using theexport
keyword, which makes them accessible from outside the namespace. To use the functions, you simply call them using the namespace name as a prefix.
Namespaces can also be nested inside other namespaces, and can be split across multiple files using the
/// <reference path="filename.ts" />
syntax.
To use ES modules in TypeScript, you can use the
import
andexport
keywords to define and use modules.
// my-module.ts
export function myFunction() {
console.log("Hello from myFunction!");
}
export const myVariable = "Hello from myVariable!";
In this example, Using the
import
keyword to import themyFunction
andmyVariable
exports from themy-module
module.
// main.ts
import { myFunction, myVariable } from "./my-module";
myFunction(); // Output: "Hello from myFunction!"
console.log(myVariable); // Output: "Hello from myVariable!"
Adding a production workflow to a TypeScript project typically involves creating a production build that is optimized for performance, and automating the build and deployment process.
To create a production build, you typically want to use a bundler like webpack or Rollup to bundle your code and assets into a single file, and use a tool like UglifyJS or Terser to minify and optimize the JavaScript code.
Here's an example
webpack.config.js
file that creates a production build:
// webpack.config.js
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
module.exports = {
mode: "production",
entry: "./src/index.ts",
output: {
filename: "bundle.js",
path: path.resolve(__dirname, "dist"),
},
module: {
rules: [
{
test: /\.tsx?$/,
use: "ts-loader",
exclude: /node_modules/,
},
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {},
},
}),
],
},
};
In this example, we use the
TerserPlugin
to minify and optimize the JavaScript code. We also set themode
option to"production"
to enable production optimizations.
Once you have a production build, you can automate the build and deployment process using a tool like GitHub Actions or Jenkins. This typically involves configuring a build script that runs the production build and deploys the built files to a server or hosting platform.
Here's an example GitHub Actions workflow that builds and deploys a TypeScript project to GitHub Pages:
name: Deploy to GitHub Pages
on:
push:
branches:
- main
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
In this example, the workflow is triggered on every push to the
main
branch. The build job checks out the code, installs dependencies,builds
the production bundle usingnpm run build
, and deploys the built files to GitHub Pages using thepeaceiris/actions-gh-pages
GitHub Action.
- Install the JavaScript library and its type definitions, if available, as a dependency using npm or yarn. For example, to install the popular
lodash
library and its type definitions:
npm install --save lodash @types/lodash
- n your TypeScript code, import the library as you would in JavaScript. For example, to use the
map
function fromlodash
:
import * as _ from "lodash";
const numbers = [1, 2, 3];
const doubledNumbers = _.map(numbers, (n) => n * 2);
Note that we use the
import * as
syntax to import the entire library as a namespace, which allows us to access its functions and properties using dot notation. 3. If the library doesn't have its own type definitions, or if the existing type definitions are incomplete or incorrect, you can create your own type definitions. This involves creating a.d.ts
file that declares the types for the library's functions and properties. For example, here's a simple type definition for themap
function fromlodash
:
declare module "lodash" {
function map<T, U>(collection: T[], iteratee: (value: T) => U): U[];
}
This declares a new module called
"lodash"
, and adds a type declaration for themap
function that accepts an arraycollection
and aniteratee
function that transforms each element of the array. You can add more type declarations as needed. 4. Finally, you need to configure your TypeScript compiler to recognize the library and its type definitions. To do this, add the library to theinclude
array in yourtsconfig.json
file, and add any necessary compiler options. For example:
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"strict": true,
"esModuleInterop": true
},
"include": [
"src/**/*",
"node_modules/@types/lodash/index.d.ts"
]
}
In this example, we specify the target version of ECMAScript, the module system, and some strict compiler options. We also set
esModuleInterop
totrue
, which allows us to use default exports from CommonJS modules (which is the module system used by many JavaScript libraries). Finally, we include the"@types/lodash"
module in the include array to tell TypeScript where to find the type definitions forlodash
.
class-transformer
is a library that allows you to transform plain JavaScript objects into class instances and vice versa. It can be useful in situations where you want to work with plain data objects, but still take advantage of the benefits of using classes, such as encapsulation, inheritance, and polymorphism.
One of the key features of
class-transformer
is its ability to perform type conversions automatically, without requiring you to define type annotations or interfaces for your data objects. For example, consider the following plain JavaScript object:
const data = {
name: "Alice",
age: "30",
address: {
street: "123 Main St",
city: "Anytown",
state: "CA",
zip: "12345",
},
};
To convert this object into a class instance, you can define a class that mirrors the shape of the object:
class Person {
name: string;
age: number;
address: {
street: string;
city: string;
state: string;
zip: string;
};
}
Then, you can use the
plainToClass
function fromclass-transformer
to convert the object to an instance of thePerson
class:
import { plainToClass } from "class-transformer";
const person = plainToClass(Person, data);
console.log(person instanceof Person); // true
console.log(person.name); // "Alice"
console.log(person.age); // 30 (automatically converted from a string)
console.log(person.address.street); // "123 Main St"
Similarly, you can use the
classToPlain
function to convert a class instance back to a plain JavaScript object:
import { classToPlain } from "class-transformer";
const plainData = classToPlain(person);
console.log(typeof plainData.age); // "string" (automatically converted back from a number)
console.log(plainData.address.city); // "Anytown"
class-transformer
also supports a variety of advanced features, such as custom transformations, exclusion and inclusion strategies, circular references, and more. For more information, see theclass-transformer
documentation.
class-validator
is a TypeScript library that provides a set of decorators and validator functions to validate class instances and plain JavaScript objects.
One of the key features of
class-validator
is its ability to validate complex object structures, including nested objects, arrays, and maps. To useclass-validator
, you can define a class or interface that mirrors the structure of the object you want to validate, and then decorate its properties with validation decorators, such asIsNotEmpty
,IsEmail
,IsString
, and many others. For example:
import { IsEmail, IsNotEmpty, IsString } from "class-validator";
class User {
@IsNotEmpty()
@IsString()
name: string;
@IsNotEmpty()
@IsEmail()
email: string;
}
Then, you can use the
validate
function fromclass-validator
to validate an instance of theUser
class:
import { validate } from "class-validator";
const user = new User();
user.name = "Alice";
user.email = "alice.example.com"; // invalid email format
validate(user).then((errors) => {
console.log(errors.length); // 1
console.log(errors[0].constraints); // { isEmail: 'email must be an email' }
});
You can also validate plain JavaScript objects by using the
validateSync
function, which returns an array of validation errors:
const data = {
name: "Bob",
email: "bob@example.com",
};
const errors = validateSync(data);
console.log(errors.length); // 0 (valid)
class-validator
also supports a variety of advanced features, such as custom validation rules, conditional validation, message translation, and more. For more information, see theclass-validator
documentation.
In React with TypeScript, you can use refs to get user input from form elements (e.g. input, select) and use it in your components. Here's an example:
import { useRef } from 'react';
function MyForm() {
const inputRef = useRef<HTMLInputElement>(null);
function handleSubmit(event: React.FormEvent<HTMLFormElement>) {
event.preventDefault();
const inputValue = inputRef.current?.value;
console.log(inputValue);
}
return (
<form onSubmit={handleSubmit}>
<label>
Name:
<input type="text" ref={inputRef} />
</label>
<button type="submit">Submit</button>
</form>
);
}
In the example above, the
MyForm
component uses theuseRef
hook to create a reference to theinput
element. The type of the reference is defined asHTMLInputElement
since we know that the ref is attached to aninput
element.
The
handleSubmit
function is called when the form is submitted. It prevents the default form submission behavior and gets the value of the input field using thecurrent
property of theinputRef
reference. Thecurrent
property may be null so we use the optional chaining operator(?.)
to avoid errors.
Finally, the input element uses the
ref
prop to attach the reference to the input element.