DEV Community

Cover image for Relearning the Basics of TypeScript
Kenta Takeuchi
Kenta Takeuchi

Posted on • Originally published at bmf-tech.com

Relearning the Basics of TypeScript

This article was originally published on bmf-tech.com.

Overview

Revisiting the basics of TypeScript.

Review of JavaScript

Variable Scope

Global Scope

Defined as properties of the window object.

const a = "Hello";
console.log(window.a); // Hello
Enter fullscreen mode Exit fullscreen mode

Local Scope

Function Scope

Variables defined within a function are only valid within that function.

function func() {
    const a = "Hello";
    console.log(a); // Hello
}
Enter fullscreen mode Exit fullscreen mode

Lexical Scope

When a function is defined within another function, the inner function can access the variables of the outer function.

function outer() {
    const a = "Hello";
    function inner() {
        console.log(a); // Hello
    }
    inner();
}
Enter fullscreen mode Exit fullscreen mode

Block Scope

Variables defined within a block like an if statement or a for loop are only valid within that block.

if (true) {
    const a = "Hello";
    console.log(a); // Hello
}
Enter fullscreen mode Exit fullscreen mode

const is Non-reassignable but Not Immutable

const obj = { key: "value"};
obj = { key: "newValue" }; // Error
obj.key = "newValue"; // OK
Enter fullscreen mode Exit fullscreen mode

Issues with var

  • Allows redeclaration of variables with the same name
  • Can overwrite global variables
  • Risk of bugs due to variable hoisting
  • Wide scope
    • Function scope, not block scope

Boxing

Converting primitive types to object types.

const a = "Hello";
const aobj = new String(a);
aobj.length; // 5
Enter fullscreen mode Exit fullscreen mode

Primitive types do not have fields or methods, so boxing is necessary, but in JavaScript, it is done implicitly. This is called auto-boxing.

const a = "Hello";
a.length; // 5
Enter fullscreen mode Exit fullscreen mode

The object resulting from auto-boxing is called a wrapper object. For example, Boolean is the wrapper object for boolean. There are no wrapper objects for undefined and null.

Objects

Everything Except Primitives is an Object

// Primitives
const num = 1;
const str = "Hello";
// etc...

// Objects
const obj = { key: "value" };
const arr = [1, 2, 3];
const func = function() { return "Hello"; };
// etc...
Enter fullscreen mode Exit fullscreen mode

Generators

Generators can return values using yield within a function.

function* gen() {
    yield 1;
    yield 2;
    yield 3;
}

const g = gen();
console.log(g.next()); // { value: 1, done: false }
Enter fullscreen mode Exit fullscreen mode

Basics of TypeScript

Type Annotation for Variable Declaration

You can assign types to variables.

const a: string = "Hello";
Enter fullscreen mode Exit fullscreen mode

You can also use wrapper objects, but wrapper object types cannot be assigned to primitive types.

const a: Number = 0;
const b: number = a; // Type 'Number' is not assignable to type 'number'.'number' is a primitive, but 'Number' is a wrapper object. Prefer using 'number' when possible.
Enter fullscreen mode Exit fullscreen mode

Also, operators cannot be used with wrapper object types.

const a: Number = 0;
const b = a + 1; // Operator '+' cannot be applied to types 'Number' and '1'.
Enter fullscreen mode Exit fullscreen mode

It is recommended to use primitive types instead of wrapper object types.

Type Inference for Variable Declaration

Types are inferred.

let a = "Hello"; // a: string
a = 1; // Type 'number' is not assignable to type 'string'
Enter fullscreen mode Exit fullscreen mode

Type Coercion

Even if the types are different, it may not result in an error.

"10" - 1; // 9
Enter fullscreen mode Exit fullscreen mode

Type coercion is the implicit conversion to another type.

Literal Types

Types that can only take specific values.

let a: "Hello" = "Hello";
a = "World"; // Type '"World"' is not assignable to type '"Hello"'.
Enter fullscreen mode Exit fullscreen mode

Primitive types that can be used as literal types are as follows.

  • string type
  • number type
  • boolean type

any Type

A type that can accept any type.

let a: any = "Hello";
a = 1; // OK
Enter fullscreen mode Exit fullscreen mode

When type inference cannot be made from context (e.g., when type annotation is omitted), it is implicitly treated as any type.

Objects

Type Annotation for Objects

const obj: { key: string } = { key: "value" };
Enter fullscreen mode Exit fullscreen mode

Method type annotation is also possible.

const obj: { key: () => string } = { key: () => "value" };
Enter fullscreen mode Exit fullscreen mode

There is also an object type, but since it represents all objects except primitive types, it is not recommended to use the object type. Also, the object type does not guarantee type safety.

const obj: object = { key: "value" };
obj.key; // Property 'key' does not exist on type 'object'.
Enter fullscreen mode Exit fullscreen mode

readonly for Object Types

A modifier to make properties read-only.

const obj: { readonly key: string } = { key: "value" };
obj.key = "newValue"; // Cannot assign to 'key' because it is a read-only property.
Enter fullscreen mode Exit fullscreen mode

It can also be written in a consolidated way.

const obj: Readonly<{
    foo: string;
    bar: number;
};>
Enter fullscreen mode Exit fullscreen mode

Optional Property for Object Types

A modifier to make object properties optional.

let obj: { key?: string } = {};
obj = {} // OK
Enter fullscreen mode Exit fullscreen mode

never Type

A type that holds no values.

function error(message: string): never {
    throw new Error(message);
}
Enter fullscreen mode Exit fullscreen mode

unknown Type

Like any type, it can accept any type, but unknown type guarantees type safety. It is used when the type is unknown.

let a: unknown = "Hello";
a = 1; // OK
const b: string = a; // Type 'unknown' is not assignable to type 'string'.
Enter fullscreen mode Exit fullscreen mode

When using unknown type, explicitly specify the type using type assertion, typeof, or instanceof.

const a: unknown = "hello";

const b: string = a as string;

if (typeof a === "string") {
  const c: string = a;
}

if (a instanceof String) {
  const d: string = a as string;
}
Enter fullscreen mode Exit fullscreen mode

Functions

Type Annotation for Function Declarations

function func(a: string, b: number): string {
    return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Type Annotation for Function Expressions

const sayHi = function(name: string): string {
    return "Hi, " + name;
}
Enter fullscreen mode Exit fullscreen mode

Type Annotation for Arrow Functions

const sayHi = (name: string): string => {
    return "Hi, " + name;
}
Enter fullscreen mode Exit fullscreen mode

Function Type Declaration

You can declare only the type of a function without implementing it.

type SayHi = (name: string) => string;
const sayHi: SayHi = (name) => {
    return "Hi, " + name;
}
Enter fullscreen mode Exit fullscreen mode

Method syntax is also possible.

type Obj = {
    sayHi: (name: string) => string;
}
Enter fullscreen mode Exit fullscreen mode

Type Guard Functions

Functions that determine the type when it is unknown.

// a is string part is called type predicate
function isString(a: unknown): a is string {
    return typeof a === "string";
}

const a: unknown = "Hello";
if (isString(a)) {
    const b: string = a;
}
Enter fullscreen mode Exit fullscreen mode

Assertion Functions

Functions that perform type assertions.

function isString(a: unknown): asserts a is string {
    if (typeof a !== "string") {
        throw new Error("Type assertion failed.");
    }
}

const a: unknown = "Hello";
isString(a);
Enter fullscreen mode Exit fullscreen mode

Overload Functions

Defining multiple functions with the same name that take different types of arguments.

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

add(1, 2); // 3
add("Hello", "World"); // HelloWorld
Enter fullscreen mode Exit fullscreen mode

Classes

Type Annotation for Classes

const animal: Animal = new Animal();
Enter fullscreen mode Exit fullscreen mode

Type Annotation for Class Constructors

class Animal {
    constructor(name: string) {
        this.name = name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Type Annotation for Class Methods

class Animal {
    sayHi(name: string): string {
        return "Hi, " + name;
    }
}
Enter fullscreen mode Exit fullscreen mode

Nominal Type

Treating types as different even if they have the same name.

TypeScript does not support nominal types, so they are treated as structural types.

type Animal = {
    name: string;
}

type Person = {
    name: string;
}

const animal: Animal = { name: "Taro" };
Enter fullscreen mode Exit fullscreen mode

To achieve nominal types, you need to change the structure (e.g., add properties).

Open-ended and Declaration Merging

Open-ended means that defining multiple interfaces with the same name does not result in a duplication error.

Declaration merging means that defining multiple interfaces with the same name results in them being merged.

interface Animal {
    name: string;
}

interface Animal {
    age: number;
}

const animal: Animal = { name: "Taro", age: 3 };
Enter fullscreen mode Exit fullscreen mode

These features are useful, for example, when extending library type definitions.

By splitting type definition files, you can import only the necessary types.

Reusing Types

typeof

Get the type of a variable.

const a = "Hello";
type A = typeof a; // type A = string;
Enter fullscreen mode Exit fullscreen mode

keyof

Get property names as types from an object type.

type Animal = {
  name: string;
  age: number;
};

type AnimalKey = keyof Animal; // type AnimalKey = "name" | "age";
Enter fullscreen mode Exit fullscreen mode

Utility Types

Required

Make all properties required (≒ remove optional).

type Animal = {
  name?: string;
  age?: number;
};

type RequiredAnimal = Required<Animal>; // type RequiredAnimal = { name: string; age: number; };
Enter fullscreen mode Exit fullscreen mode

Readonly

Make all properties read-only.

type Animal = {
  name: string;
  age: number;
};

type ReadonlyAnimal = Readonly<Animal>; // type ReadonlyAnimal = { readonly name: string; readonly age: number; };
Enter fullscreen mode Exit fullscreen mode

Partial

Make all properties optional.

type Animal = {
  name: string;
  age: number;
};

type PartialAnimal = Partial<Animal>; // type PartialAnimal = { name?: string; age?: number; };
Enter fullscreen mode Exit fullscreen mode

Record

Generate an object type where the property keys and values are Keys and Type, respectively.

type Name = string
type Age = number
type AnimalRecord = Record<Name, Age>; // type AnimalRecord = { [key: string]: number; };
Enter fullscreen mode Exit fullscreen mode

Pick

Extract properties from type T specified by Keys.

type Animal = {
  name: string;
  age: number;
};

type Name = Pick<Animal, "name">; // type Name = { name: string; };
Enter fullscreen mode Exit fullscreen mode

Omit

Exclude properties from type T specified by Keys.

type Animal = {
  name: string;
  age: number;
};

type Name = Omit<Animal, "age">; // type Name = { name: string; };
Enter fullscreen mode Exit fullscreen mode

Exclude

Generate a union type by excluding types specified by U from type T.

type Animal = "dog" | "cat" | "rabbit";

type ExcludeAnimal = Exclude<Animal, "dog">; // type ExcludeAnimal = "cat" | "rabbit";
Enter fullscreen mode Exit fullscreen mode

Extract

Generate a union type by extracting types specified by U from type T.

type Animal = "dog" | "cat" | "rabbit";

type ExtractAnimal = Extract<Animal, "dog">; // type ExtractAnimal = "dog";
Enter fullscreen mode Exit fullscreen mode

NoInfer

Prevent type inference for type T.

type Animal = {
  name: string;
  age: number;
};

function getAnimal<T>(animal: T): T {
  return animal;
}

const animal = getAnimal<NoInfer<Animal>>({ name: "Taro", age: 3 });
Enter fullscreen mode Exit fullscreen mode

Mapped Types

Generate a new type based on a specified type.

type Animal = {
  name: string;
  age: number;
};

type ReadonlyAnimal = {
  readonly [K in keyof Animal]: Animal[K];
};
Enter fullscreen mode Exit fullscreen mode

Indexed Access Types

Get the type of a property or array element.

type Animal = {
  name: string;
  age: number;
};

type Name = Animal["name"]; // type Name = string;

type ArrayType = string[];
type ElementType = ArrayType[number]; // type ElementType = string;
Enter fullscreen mode Exit fullscreen mode

Conditional Types

Change types based on conditions.

type IsString<T> = T extends string ? "string" : "not string";

type A = IsString<string>; // type A = "string";
Enter fullscreen mode Exit fullscreen mode

infer

A type operator used in conditional types to obtain a type variable.

// Utility type to extract the return type of a function

type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

// Example function
function exampleFunction(): string {
  return "Hello, World!";
}

// Get the return type of the function
type ExampleFunctionReturnType = MyReturnType<typeof exampleFunction>;

// ExampleFunctionReturnType is of type string
const exampleReturnValue: ExampleFunctionReturnType = "This is a string";

console.log(exampleReturnValue); // This is a string
Enter fullscreen mode Exit fullscreen mode

Union Distribution

Distribute a union type and apply it to each type.

type A = "a" | "b";

type B = A extends "a" ? "c" : "d"; // type B = "c" | "d";
Enter fullscreen mode Exit fullscreen mode

Generics

Types that accept types as arguments.

// Generics
function identity<T>(arg: T): T {
  return arg;
}

const a = identity<string>("Hello");
const b = identity<number>(1);

// Type arguments
type Identity<T> = T;
type A = Identity<string>; // type A = string;
Enter fullscreen mode Exit fullscreen mode

Thoughts

I have studied JavaScript several times before, but I felt like JavaScript was this difficult even before TypeScript...

References

Top comments (0)