Categories
JavaScript TypeScript

TypeScript Advanced Types — Conditional Types

TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There are multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more.

In this article, we’ll look at conditional types.

Conditional Types

Since TypeScript 2.8, we can define types with conditional tests. This lets us add types to data that can have different types according to the condition we set. The general expression for defining a conditional type in TypeScript is the following:

T extends U ? X : Y

T extends U describes the relationship between the generic types T and U . If T extends U is true then the X type is expected. Otherwise, the Y type is expected. For example, we can use it as in the following code:

interface Animal {    
  kind: string;  
}

interface Cat extends Animal {  
  name: string;  
}

interface Dog {  
  name: string;  
}

type CatAnimal = Cat extends Animal ? Cat : Dog;  
let catAnimal: CatAnimal = <Cat>{  
  name: 'Joe',  
  kind: 'cat'  
}

In the code above, we created the CatAnimal type alias which is set to the Cat type if Cat extends Animal . Otherwise, it’s set to Dog . Since Cat does extend Animal , the CatAnimal type alias is set to the Cat type.

This means that in the example above if we change <Cat> to <Dog> like we do in the following code:

interface Animal {    
  kind: string;  
}

interface Cat extends Animal {  
  name: string;  
}

interface Dog {  
  name: string;  
}

type CatAnimal = Cat extends Animal ? Cat : Dog;  
let catAnimal: CatAnimal = <Dog>{  
  name: 'Joe',  
  kind: 'cat'  
}

We would get the following error message:

Property 'kind' is missing in type 'Dog' but required in type 'Cat'.(2741)

This ensures that we have the right type for catAnimal according to the condition expressed in the type. If we want to Dog to be the type for catAnimal , then we can write the following instead:

interface Animal {    
  kind: string;  
}

interface Cat  {  
  name: string;  
}

interface Dog extends Animal {  
  name: string;  
}

type CatAnimal = Cat extends Animal ? Cat : Dog;  
let catAnimal: CatAnimal = <Dog>{  
  name: 'Joe'  
}

We can also have nested conditions to determine the actual type from multiple conditions. For example, we can write:

interface Animal {    
  kind: string;  
}

interface Bird  {  
  name: string;  
}

interface Cat  {  
  name: string;  
}

interface Dog extends Animal {  
  name: string;  
}

type AnimalTypeName<T> =  
  T extends Animal ? Cat :      
  T extends Animal ? Dog :      
  T extends Animal ? Bird :  
  Animaltype t0 = AnimalTypeName<Cat>;    
type t1 = AnimalTypeName<Dog>;  
type t2 = AnimalTypeName<Animal>;  
type t3 = AnimalTypeName<Bird>;

Then we get the following types for the type alias t0 , t1 , t2 , and t3 :

type t0 = Animal  
type t1 = Cat  
type t2 = Cat  
type t3: Animal

The exact doesn’t have to be chosen immediately, we can also have something like:

interface Foo {}

interface Bar extends Foo {  
    
}

function bar(x) {  
  return x;  
}

function foo<T>(x: T) {  
  let y: T extends Foo ? string : number = bar(x);  
  let z: string | number = y;  
}

foo<Bar>(1);  
foo<Bar>('1');  
foo<Bar>(false);

As we can see we can pass in anything into the foo even though we have the conditional types set. This is because the actual type in the type condition hasn’t been chosen yet., so TypeScript doesn’t make any assumption about what we can assign to the variables in the foo function.

Distributive Conditional Types

Conditional types are distributive. If we have multiple conditional types that can possibly extend one type as we have in the following code:

interface A {}  
interface B {}  
interface C {}  
interface D {}  
interface X {}  
interface Y {}type TypeName = (A | B | C) extends D ? X : Y;

Then the last line is equivalent to:

(A extends D ? X : Y) | (B extends D ? X : Y) | (C extends D ? X : Y)

For example, we can use it to filter out types with various conditions. For example, we can write:

type Diff<T, U> = T extends U ? never : T;

To remove types from T that are assignable to U . If T extends U, then the Diff<T, U> type is never, which means that we can assign anything to it, otherwise it takes on the type T. Likewise, we can write:

type Filter<T, U> = T extends U ? T : never;

to remove types from T that aren’t assignable to U . In this case, if T extends U, then the Filter type is the same as the T type, otherwise, it takes on the never type. For example, if we have:

type Diff<T, U> = T extends U ? never : T;  
type TypeName = Diff<string| number | boolean, boolean>;

Then TypeName has the type string | number . This is because Diff<string| number | boolean, boolean> is the same as:

(string extends boolean ? never : string) | (number extends boolean ? never: number) | (boolean extends boolean ? never: boolean)

On the other hand, if we write:

type Filter<T, U> = T extends U ? T : never;  
type TypeName = Filter<string| number | boolean, boolean>;

Then TypeName has the boolean type. This is because Diff<string| number | boolean, boolean> is the same as:

(string extends boolean ? string: never) | (number extends boolean ? number: never) | (boolean extends boolean ? boolean: never)

Predefined Conditional Types

TypeScript 2.8 has the following predefined conditional types, They’re the following:

  • Exclude<T, U> – excludes from T those types that are assignable to U.
  • Extract<T, U> – extract from T those types that are assignable to U.
  • NonNullable<T> – exclude null and undefined from T.
  • ReturnType<T> – get the return type of a function type.
  • InstanceType<T> – get the instance type of a constructor function type.

Since TypeScript 2.8, we can define types with conditional tests. The general expression for defining a conditional type in TypeScript is T extends U ? X : Y . They’re distributive, so (A | B | C) extends D ? X : Y; is the same as (A extends D ? X : Y) | (B extends D ? X : Y) | (C extends D ? X : Y) .

Categories
JavaScript TypeScript

TypeScript Advanced Types — Literal Types and Discriminated Unions

TypeScript has many advanced type capabilities and which makes writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capability of TypeScript. There’re multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at intersection and union types.

Intersection Types

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s denoted by an & sign. All members of all the types are required in the object of an intersection type.

For example, we can use the intersection type like in the following code:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
}

let employee: Animal & Person & Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20,  
  employeeCode: '123'  
}

As we can see from the code above, each type is separated by an & sign. Also, the employee object has all the properties of Animal , Person , and Employee . Each property has a type that’s defined in each interface. If the structure doesn’t match exactly, then we’ll get error messages like the following from the TypeScript compiler:

Type '{ kind: string; firstName: string; lastName: string; age: number; }' is not assignable to type 'Animal & Person & Employee'.Property 'employeeCode' is missing in type '{ kind: string; firstName: string; lastName: string; age: number; }' but required in type 'Employee'.(2322)input.ts(12, 3): 'employeeCode' is declared here.

The error above occurs if we have the following code:

let employee: Animal & Person & Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20    
}

TypeScript looks for the employeeCode property since the employeeCode property is in the Employee interface.

If 2 types have the same member name but different type, then it’s automatically assigned the never type, when they’re joined together as an intersection type. This means that we can’t assign anything to it. For example, if we have:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
  age: string;  
}

let employee: Animal & Person & Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20  
}

Then we get the following error from the TypeScript compiler:

Type 'number' is not assignable to type 'never'.(2322)input.ts(8, 3): The expected type comes from property 'age' which is declared here on type 'Animal & Person & Employee'

If we omit the property then the compiler will also raise an error about the age property being missing. Therefore, we should never have types that have the same member name if we want to create intersection types from them.

Union Types

Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol.

For example, we can define an object that has a union type like in the following code:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20    
}

The code above has an employee object of the Animal | Person | Employee type which means that it can have some of the properties of the Animal, Person, or Employee interfaces. Not all of them have to be included, but if they’re included, then the type has to match the ones in the interface.

With union types, we can have 2 types that have the same member name but with different types. For example, if we have the following code:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
  age: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: '20'  
}

Then we can assign both a number or a string to the age property. This fits with the dynamic nature of JavaScript while letting us assign data types to objects. This is different from traditional object-oriented code where we may abstract common members into a parent class and then derive sub-classes from it which has specialized members.

The pipe symbol means the object of a union type can take on none, some, or all properties of each type.

Accessing members in an object of a union type is different from how members are accessed from an intersection type. Objects that have intersection types have to have all the properties listed in the members of each type, so logically, we can access all the properties that are defined in the object. However, this isn’t the case with union types since some members may be available to only some of the types that make up the union type.

If we have a union type, then we can only access members that are available in all the types that form the union type. For example, if we have:

interface Animal {  
  kind: string;  
}

interface Person {  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  employeeCode: string;  
  age: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: '20'  
}

console.log(employee)

Then we can’t access any properties of the employee object since none of the members are available in all the types. If we try to access a property like the kind property, we’ll get the following error:

Property 'kind' does not exist on type 'Animal | Person | Employee'.Property 'kind' does not exist on type 'Person'.(2339)

If we want to make some property accessible, we can write something like the following code to let us access a property:

interface Animal {  
  kind: string;  
}

interface Person {  
  kind: string;  
  firstName: string;  
  lastName: string;  
  age: number;  
}

interface Employee {  
  kind: string;  
  employeeCode: string;  
}

let employee: Animal | Person | Employee = {  
  kind: 'human',  
  firstName: 'Jane',  
  lastName: 'Smith',  
  age: 20,  
  employeeCode: '123'  
}

console.log(employee.kind)

In all 3 interfaces above, we have the kind member. Since they’re in all 3 interfaces, which we used in the union type, we can access the employee.kind property. Then we would get the text ‘human’ in the console.log statement.

An intersection type lets us combine multiple types into one. The structure of an object that has an intersection type has to have both the structure of all the types that form the intersection types. It’s formed by joining multiple types by an & sign. Union types create a new type that lets us create objects that have some or all of the properties of each type that created the union type. Union types are created by joining multiples with the pipe | symbol. It lets us create a new type that has some of the structure of each type that forms the union type.

Categories
Angular JavaScript TypeScript

How to do Localization and Translation in Angular

With the modular structure of Angular apps, adding localization and translation features to it is easy. The most popular Angular add-on is ngx-translate . Angular 2+ is supported by ngx-translate .

Given an existing Angular app, we can install ngx-translate by running npm i ngx-translate .

After installing the package, make sure you have the following in your Angular module file. For example, in app.module.ts , we have:

export function HttpLoaderFactory(http: HttpClient) {  
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');  
}

This allow us to load our JSON translation files from the ./assets/i18n/ . The JSON file should be named according to standard language codes listed in the ‘639–1’ column in the table of https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes. ngx-translate will load it into the Angular app, with its HTTP loader via a GET request.

app.module.ts should look something this, given all of these components and libraries are included in the app:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { HttpModule } from '@angular/http';
import { RouterModule, Routes } from '@angular/router';
import { PaginationModule } from 'ngx-bootstrap/pagination';
import { AppComponent } from './app.component';
import { AccountPageComponent } from './account-page/account-page.component';
import { LoginPageComponent } from './login-page/login-page.component';
import { RegisterPageComponent } from './register-page/register-page.component';
import { MenuPageComponent } from './menu-page/menu-page.component';
import { OrdersPageComponent } from './orders-page/orders-page.component';
import { CategoryPageComponent } from './category-page/category-page.component';
import { AboutPageComponent } from './about-page/about-page.component';
import { HomePageComponent } from './home-page/home-page.component';
import { OrderService } from './order.service';
import { MenuService } from './menu.service';
import { UserService } from './user.service';
import { LocationService } from './location.service';
import { CategoryService } from './category.service';
import { RestaurantService } from './restaurant.service';
import { RestaurantsService } from './restaurants.service';
import { IsLoggedInGuard } from './is-logged-in.guard';
import { BottomBarComponent } from './bottom-bar/bottom-bar.component';
import { CartPageComponent } from './cart-page/cart-page.component';
import { VerifyAccountPageComponent } from './verify-account-page/verify-account-page.component';
import { SearchPageComponent } from './search-page/search-page.component';
import { RestaurantPageComponent } from './restaurant-page/restaurant-page.component';
import { ItemPageComponent } from './item-page/item-page.component';
import { ForgotPasswordPageComponent } from './forgot-password-page/forgot-password-page.component';
import { ResetPasswordPageComponent } from './reset-password-page/reset-password-page.component';
import { HttpClientModule, HttpClient } from '@angular/common/http';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
export function HttpLoaderFactory(http: HttpClient) {
  return new TranslateHttpLoader(http, './assets/i18n/', '.json');
}
const appRoutes: Routes = [
  { path: 'category/:category', component: CategoryPageComponent },
  { path: 'login', component: LoginPageComponent },
  { path: 'verifyaccount/:userId/:verifyToken', component: VerifyAccountPageComponent },
  { path: 'register', component: RegisterPageComponent, },
  { path: 'resetpassword/:userId/:resetToken', component: ResetPasswordPageComponent, },
  { path: 'cart', component: CartPageComponent, },
  { path: 'forgotpassword', component: ForgotPasswordPageComponent, },
  { path: 'restaurant/:id', component: RestaurantPageComponent },
  { path: 'item/:restaurantId/:menuId/:itemId', component: ItemPageComponent },
  { path: 'menu', component: MenuPageComponent, canActivate: [IsLoggedInGuard] },
  { path: 'orders', component: OrdersPageComponent, canActivate: [IsLoggedInGuard] },
  { path: 'about', component: AboutPageComponent, },
  { path: 'search/:address', component: SearchPageComponent, },
  { path: 'account', component: AccountPageComponent, canActivate: [IsLoggedInGuard] },
  { path: '**', component: HomePageComponent }
];
@NgModule({
  declarations: [
    AppComponent,
    AccountPageComponent,
    LoginPageComponent,
    RegisterPageComponent,
    MenuPageComponent,
    OrdersPageComponent,
    CategoryPageComponent,
    AboutPageComponent,
    HomePageComponent,
    BottomBarComponent,
    CartPageComponent,
    VerifyAccountPageComponent,
    SearchPageComponent,
    RestaurantPageComponent,
    ItemPageComponent,
    ForgotPasswordPageComponent,
    ResetPasswordPageComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    RouterModule.forRoot(
      appRoutes, { useHash: true }
    ),
    PaginationModule.forRoot(),
    HttpClientModule,
    TranslateModule.forRoot({
      loader: {
        provide: TranslateLoader,
        useFactory: HttpLoaderFactory,
        deps: [HttpClient]
      }
    })
  ],
  providers: [
    RestaurantService,
    RestaurantsService,
    OrderService,
    MenuService,
    UserService,
    LocationService,
    CategoryService,
    IsLoggedInGuard
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The JSON should be something like this (en.json):

{  
    "Chiyoda": "Chiyoda",  
    "Chūō": "Chūō",  
    "Minato": "Minato",  
    "Shinjuku": "Shinjuku",  
    "Bunkyō": "Bunkyō",  
    "Taitō": "Taitō",  
    "Sumida": "Sumida",  
    "Kōtō": "Kōtō",  
    "Shinagawa": "Shinagawa",  
    "Meguro": "Meguro",  
    "Ōta": "Ōta",  
    "Setagaya": "Setagaya",  
    "Shibuya": "Shibuya",  
    "Nakano": "Nakano",  
    "Suginami": "Suginami",  
    "Toshima": "Toshima",  
    "Kita": "Kita",  
    "Arakawa": "Arakawa",  
    "Itabashi": "Itabashi",  
    "Nerima": "Nerima",  
    "Adachi": "Adachi",  
    "Katsushika": "Katsushika",  
    "Edogawa": "Edogawa",  
    "Select a Location": "Select a Location",  
    "City": "City",  
    "Ward": "Ward",  
    "Submit": "Submit",  
    "Buffet": "Buffet",  
    "Office Packages": "Office Packages",  
    "Family Event": "Family Event",  
    "Seminar": "Seminar",  
    "High tea and dessert table": "High tea and dessert table",  
    "Christmas": "Christmas",  
    "Kids Party": "Kids Party",  
    "Wedding": "Wedding",  
    "Become a partner": "Become a partner",  
    "About": "About"  
}

and foreign language file (jp.json ):

{  
    "Chiyoda": "千代田区",  
    "Chūō": "中央区",  
    "Minato": "港区",  
    "Shinjuku": "新宿区",  
    "Bunkyō": "文京区",  
    "Taitō": "台東区",  
    "Sumida": "墨田区",  
    "Kōtō": "江東区",  
    "Shinagawa": "品川区",  
    "Meguro": "目黒区",  
    "Ōta": "大田区",  
    "Setagaya": "世田谷区",  
    "Shibuya": "渋谷区",  
    "Nakano": "中野区",  
    "Suginami": "杉並区",  
    "Toshima": "豊島区",  
    "Kita": "北区",  
    "Arakawa": "荒川区",  
    "Itabashi": "板橋区",  
    "Nerima": "練馬区",  
    "Adachi": "足立区",  
    "Katsushika": "葛飾区",  
    "Edogawa": "江戸川区",  
    "Select a Location": "地域を選択",  
    "City": "都市",  
    "Ward": "市区町村",  
    "Submit": "検索",  
    "Buffet": "ビュッフェ",  
    "Office Packages": "お弁当",  
    "Family Event": "家族行事",  
    "Seminar": "セミナー",  
    "High tea and dessert table": "デザート",  
    "Christmas": "クリスマス",  
    "Kids Party": "子供向けパーティー",  
    "Wedding": "ウェディング",  
    "Become a partner": "パートナー提携",  
    "About": "私たちについて"  
}

You can also nest your translations in a lower level in your JSON file, like so:

{  
  "text":{      
    "Chiyoda": "千代田区",  
    "Chūō": "中央区",  
    "Minato": "港区",  
    "Shinjuku": "新宿区",  
    "Bunkyō": "文京区",  
    "Taitō": "台東区",  
    "Sumida": "墨田区",  
    "Kōtō": "江東区",  
    "Shinagawa": "品川区",  
    "Meguro": "目黒区",  
    "Ōta": "大田区",  
    "Setagaya": "世田谷区",  
    "Shibuya": "渋谷区",  
    "Nakano": "中野区",  
    "Suginami": "杉並区",  
    "Toshima": "豊島区",  
    "Kita": "北区",  
    "Arakawa": "荒川区",  
    "Itabashi": "板橋区",  
    "Nerima": "練馬区",  
    "Adachi": "足立区",  
    "Katsushika": "葛飾区",  
    "Edogawa": "江戸川区",  
    "Select a Location": "地域を選択",  
    "City": "都市",  
    "Ward": "市区町村",  
    "Submit": "検索",  
    "Buffet": "ビュッフェ",  
    "Office Packages": "お弁当",  
    "Family Event": "家族行事",  
    "Seminar": "セミナー",  
    "High tea and dessert table": "デザート",  
    "Christmas": "クリスマス",  
    "Kids Party": "子供向けパーティー",  
    "Wedding": "ウェディング",  
    "Become a partner": "パートナー提携",  
    "About": "私たちについて"  
  }  
}

Then in your template, you use the keys with the translate filter in to get translated text:

{{'Select a Location' | translate}}

If the translations in JSON is nested in another property, you can display them like so:

{{'text.About' | translate}}

Note that you cannot have spaces in your keys if you use the dot notation.

With this, the Select a Location key is looked up in the JSON, and depending on the language you set, it’ll get the right text from the value gotten from the key. If the value does not existing for the given key, then it will display the translate key.

Language can be changed in the code by injecting the TranslateService into your code. For example, in your component code, you do the following:

constructor(private translate: TranslateService)

Then you can do:

this.translate.use('jp');

to set the app to display Japanese.

You can also display the default language like so:

this.translate.setDefaultLang('jp');

Translations in Angular is straightforward. It is just a matter of installing a new package, adding the translation, setting the language, and make sure that it displays in the template.

Categories
JavaScript TypeScript

Introduction to TypeScript Functions 

Functions are small blocks of code that takes in some inputs and may return some output or have side effects. A side effect is when a function modifies some variable outside the function. We need functions to organize code into small blocks that are reusable. Without functions, if we want to re-run a piece of code, we have to copy it in different places. Functions are critical to any TypeScript program. In this article, we look at how to define TypeScript functions, how to add types to functions, and passing in arguments in different ways.

Defining Functions

To define a function in TypeScript, we can do the following:

function add(a: number, b: number){  
  return a + b;  
}

The function above adds 2 numbers together and returns the sum. a and b are parameters, which lets us pass in arguments into the function to compute and combine them. add is the name of the function, which is optional in JavaScript. The return statement lets the result of the function be sent to another variable or as an argument of another function. This is not the only way to define a function. Another way is to write it as an arrow function:

const add = (a: number, b: number) => a + b;

The 2 are equivalent. However, if we have to manipulate this in the function, then the 2 aren’t the same since the arrow function will not change the value of this inside the function, like the function defined with the function keyword does. We can assign the function to a variable since functions are objects in TypeScript. Note that the function above only works if it’s one line since the return is implicitly done if the arrow function is one line. If it’s more than one line, then we write it with brackets like so:

const add = (a: number, b: number) => {  
  return a + b;  
}

This way we have to write the return statement explicitly. Otherwise, it won’t return a value.

In both examples above, we have the parameter and then the type following each parameter. This is because in TypeScript, if a parameter isn’t followed by a type, then the type will be inferred as having the any type, which will be rejected if we have the noImplicitAny flag set when we compile the code.

Function Types

To add more checks for the types of the parameters that are passed in and also the return type of the function, we can add a type designation to the function explicitly. We can do this by writing the signature and then adding the fat right arrow, and then appending the return type of the function after that. For example, we can write the following to designate the type of our add function:

const add: (a: number, b: number) => number =  
  (a: number, b: number) => a + b;

The type designation of the add function is:

(a: number, b: number) => number

in the code above. If we put different types for the parameter than the ones designated by the type we wrote, then the TypeScript compiler will raise an error and refuse to compile the code. For example, if we write:

const add: (a: number, b: number) => number =  
  (a: number, b: string) => a + b;

Then we get the error:

Type '(a: number, b: string) => string' is not assignable to type '(a: number, b: number) => number'.Types of parameters 'b' and 'b' are incompatible.Type 'number' is not assignable to type 'string'.(2322)

from the TypeScript compiler. If we don’t write out the type explicitly, TypeScript is still smart enough to infer the type from what’s given on the right side of the assignment operator, so we don’t have to write it out explicitly. It saves us effort from having to type everything to use TypeScript.

Calling a Function

If we are referencing and using a function, then we are calling a function.

To call the function, we write:

add(1, 2) // 3

Since our function returns a value, if we console log the return value of the add function:

console.log(add(1, 2)) // logs 3

We can also assign the return value to a variable:

const sum = add(1, 2);  
console.log(sum) // logs 3

Part of a Function

All functions have some or all of the following parts:

  • function keyword, which is optional
  • the function name, which is optional
  • parentheses — this is required for any function.
  • the parameter inside the parentheses, which is optional
  • opening and closing curly brackets — required for all functions except for single line arrow functions
  • return statement, which is optional. If a function doesn’t have a return statement, it returns undefined . A return statement which has nothing following it will end the executing of the function, so it’s handy for controlling the flow of your function.

Using Arguments

As we can see, many functions have arguments. Arguments are data that is passed into a function for computation, so if we have a function call add(1, 2), then 1 and 2 are the arguments.

On the other hand, parameters are what we write in the parentheses when we define a function to clarify what we can pass in as arguments.

We do not have to define a function parameters to pass in arguments since we have the arguments object in each function. However, it is not recommended since it’s unclear what you want to pass in. We can use the arguments for optional things that we want to pass in, however.

For example, if we go back to the add function we defined above:

function add(a: number, b: number){  
  return a + b;  
}

a and b are parameters. When we call it by writing add(1, 2). 1 and 2 are arguments.

We can specify up to 255 parameters when we define a function. However, we shouldn’t do define more than 5 usually since it becomes hard to read.

Pass in Arguments by Value

There are 2 ways to pass in arguments in TypeScript. One way is to pass in arguments by value. Passing by value means that the arguments passed in are completely separate from the variables you pass in.

The content of the variable is copied into the argument and is completely separate from the original variable if we pass in a variable to a function.

The original variable won’t change even if we change the argument variables that we passed in.

Primitive data types like string, number, boolean, undefined and the null object is passed in by value in TypeScript.

For instance, if we have the following code:

const a = 1;  
const addOne = (num: number) => num + 1  
const b = addOne(a);  
console.log(b);

a would still be 1 even after we call addOne on a since we make a copy of a and set it to num when we are passing in a as the argument for the call to addOne .

Pass in Arguments by Reference

Non-primitive are passed into a function by reference, which means that the reference to the object passed in as the argument is passed into the function. The copy of the content is not made for the argument and the passed in object is modified directly.

For example, if we have the following code:

let changeObj = (obj: { foo: string }) => obj.foo = 'bar'  
const obj = {  
  foo: 'baz'  
}  
changeObj(obj);  
console.log(obj); // logs {foo: "bar"}

The original obj object is defined as { foo: 'baz' }. However, when we pass obj into the changeObj function, where the passed in obj argument is changed in place. The original obj object that we passed in is changed. obj.foo becomes 'bar' instead of 'baz' as it’s originally defined.

Missing Arguments

You do not need to pass in all the arguments into a function in TypeScript. Whatever argument that’s not passed in will have undefined set in its place. So if you don’t pass in all the arguments, then you have to check for undefined in the arguments so that you don’t get unexpected results. For example, with the add function that we had:

function add(a, b){  
  return a + b;  
}

If we call add without the second argument by writing add(1), then we get NaN since 1 + undefined is not a number.

Default Function Parameter Values

We can set default function parameters for optional parameters to avoid unexpected results. With the add function, if we want to make b optional then we can set a default value to b. The best way to do it is:

function add(a: number, b: number = 1){  
  return a + b;  
}

In the code above, we set b to 1 so that we if we don’t pass in anything for b, we automatically get 1 by default for the b parameter. So if we run add(1) we get 2 instead of NaN .

An older, alternative way to do this is to check if the type of the parameter is undefined like so:

function add(a: number, b: number){  
  if (typeof b === 'undefined'){  
    b = 1;  
  }  
  return a + b;  
}

This achieves the same purpose as the first function, but the syntax is clumsier.

TypeScript functions allow us to organize code into small parts that can be reused. There’re many ways to define a function, but sticking to the commonly recommended ways like using arrow functions and not using arguments too much is recommended. We will continue to look at TypeScript functions in the next part of this series.

Categories
JavaScript TypeScript

Introduction to TypeScript Functions: Anonymous Functions and More

Functions are small blocks of code that take in some inputs and may return some output or have side effects. A side effect means that it modifies some variables outside the function.

We need functions to organize code into small blocks that are reusable.

Without functions, if we want to re-run a piece of code, we have to copy it in different places. Functions are critical to any TypeScript program.

In this article, we continue to look at different parts of TypeScript functions, including passing in a variable amount of arguments, recursion, function nesting, and defining functions in objects.

Calling Functions with More Arguments that Parameters

In TypeScript, we can call a function with more arguments than parameters. If we just pass them in without accessing them from the argument object, they’ll be ignored. You can get the extra arguments that aren’t in the parameters with the argument object and use them. The argument object has the parameters with numerical keys just like the indexes of an array. Another way to access extra arguments is through the rest parameter.

For example, if we call the add function with extra parameters:

function add(a: number, b: number, ...rest: any){  
  console.log(arguments);  
  return a + b;  
}  
add(1, 2, 3);

The ...rest part of the signature captures the parameters that we don’t expect to be passed in. We used the rest operator, which is indicated by the 3 periods before the word rest to indicate that there might be more parameters at the end after b. We need this in TypeScript so that we don’t get the mismatch between the number of parameters and the number of arguments passed in. In vanilla JavaScript, ...rest is optional.

In the console.log call, we should get:

0: 1  
1: 2  
2: 3

Variable Scope in Functions

Functions inside shouldn’t be accessible outside of functions unless they are global variables. We should avoid defining global variables as much as we can to prevent bugs and hard to trace errors since they can be accessed anywhere in the program. To prevent defining global variables, we should use let to define variables and const to define constants. For example, we should define functions like so:

function add(a: number, b: number){  
  let sum = a + b;  
  return sum;  
}

In this case, we have sum which is only accessible within the function since it’s defined with the let keyword.

Anonymous Functions

Anonymous are functions with no names. Since they have no name, they cannot be referenced anywhere. They are often passed into other functions as callback functions, which is called when the function is passed into an argument. However, you can assign anonymous functions into a variable so it becomes a named function.

They can also be self executing. This is means that you can define the function and make it run immediately. For example, if we write:

const sum = (function(a: number, b: number){  
  return a + b;  
})(1, 2);
console.log(sum) // log 3

We log 3 because we defined a function to add 2 numbers, and then passed in 1 and 2 as the arguments immediately after by wrapping the function in parenthesis and then passed the arguments to it.

Recursion

You can call the same function from within itself in TypeScript. This is called recursion. All recursive functions must have an end condition, which is called the base case, so that it knows when it stops executing. Otherwise, you can get a function that’s called an infinite number of times, which will crash the browser.

To write a recursive function, we can write:

function sumOfSquares(num: number): number {  
  let sum: number = Math.pow(num, 2);  
  if (num == 1) {  
    return 1  
  } else {  
    return sum + sumOfSquares(num - 1)  
  }    
}

In this example, we wrote a function to compute the sum of squares for a given number. We compute the square of num and then if we have num equal to 1 then we return 1. Otherwise, we return the sum of sum plus the result of call sumOfSquares on num — 1 . We keep reducing num so that we can reach our base case of 1, adding up the results while doing so.

Nesting Functions

Functions can be nested within each other. This means that we can define a function inside another function. For example, we can write:

function convertToChicken(name: string){  
  function getChickenName(name: string){  
    return `Chicken ${name}`;  
  }  
  return getChickenName(name)  
}

In this case, we called getChickeName inside the convertToChicken call. So if we write convertToChicken('chicken') , then we get 'Chicken chicken' since we called get getChickeName and returned the result. The scope of variables is the name. let and const are block-scoped so they cannot be accessed outside of the original function that’s defined, but they are available in the nested function, so if we have:

function convertToChicken(name: string) {  
  let originalName = name;  function getChickenName(newName: string) {  
    console.log(originalName)  
    return `Chicken ${newName}`;  
  }  
  return getChickenName(name)  
}

Then originalName will still be defined in the console.log.

Defining Function in an Object

We can define a function in an object in a few ways. We can use the function keyword or arrow function as usual, but we can also write it with a shorthand for the function keyword. For example, if we have a bird object and we want to define the chirp function, we can write:

const bird = {  
 chirp: function(){  
   console.log('chirp', this)  
  }  
}

or use the following shorthand:

const bird = {  
 chirp(){  
   console.log('chirp', this)  
  }  
}

The 2 are the same since the chirp function will have the bird object as the value of this.

On the other hand, if you use an arrow function:

const bird = {  
 chirp: () => {  
   console.log('chirp', this)  
  }  
}

We’ll get an error from the Typescript compiler because the value of this is the globalThis value, which the TypeScript compiler doesn’t allow. We get the error “The containing arrow function captures the global value of ‘this’.(7041)” when we try to compile the code above.

TypeScript functions allow us to organize code into small parts that can be reused. There’re many ways to define a function, but sticking to the commonly recommended ways like using arrow functions and not using arguments too much is recommended.