One Words

TypeScript add type system on top of JavaScript, Just one purpose, helpping yourself clear what you are doing. It just make your application to look more complicated, and NOT gonna help the business logic. But it can make your logic more easier to expose bugs.

Main Points

  • TS is SuperSet of JS.
  • TS is JS with syntax for types.
  • TS need to be compiled.
  • You can say TS is a compiler, can replace Babel.
  • The main point of TS compiler is providing Type Check.
  • TS provide more semantics.
  • TS have a big community.
  • TS is known as an Object-oriented programming language whereas JS is a prototype-based language.
  • TypeScript introduces a type system to JavaScript, enabling developers to catch errors at compile-time rather than runtime.

Key Features

  • Static Typing: TypeScript allows developers to explicitly define types for variables, function parameters, and return values. This helps in detecting errors and providing better tooling support.
  • Type Inference: TypeScript can infer types based on the values assigned to variables, reducing the need for explicit type annotations.
  • Enhanced IDE Support: TypeScript provides better autocompletion, code navigation, and refactoring capabilities in modern IDEs.
  • ECMAScript Compatibility: TypeScript is designed to be a superset of JavaScript, meaning that any valid JavaScript code is also valid TypeScript code.
  • Compiler and Language Features: TypeScript has its own compiler, which transpiles TypeScript code to plain JavaScript. It also includes additional language features like interfaces, classes, modules, and more, which are not natively available in JavaScript.
  • Benefit
    • Good for large project.
    • Refactoring
    • Code completion
    • Static typing
    • Shorthand notations
Golden Rule:

TypeScript add more type feature, type management on top of JavaScript in order to make software easier debug, more robust.

Type System

TypeScript is Structural Type System.

One of TypeScript’s core principles is that type checking focuses on the shape that values have.

Golden Rule:

In a structural type system, if two objects have the same shape, they are considered to be of the same type.

Interface vs type alias is always a topic for better understanding of TS typs system.

Normally, TypeScript will generate types for you in many cases. For example in creating a variable and assigning it to a particular value, TypeScript will use the value as its type.

let helloWorld = "Hello World";
let helloWorld: string

But types can be more complicated than that. For example, some design patterns make it difficult for types to be inferred automatically. So TS provide a list of ways to solve them.

TS provide ways to describe types of JS

  • For Primitive types, like string, number, boolean, BigInt, Symbol, null and undefined, each has a corresponding type in TypeScript.
  • To specify the type of an array like [1, 2, 3], you can use the syntax number[]. It is same with Array<number>.
  • Can and should Type Annotations on Variables.
  • Using any whenever you don’t want a particular value to cause typechecking errors.
  • Can specify the types of both the input and output values of functions.
  • Define an object type, we simply list its properties and their types. And it support Optional Properties.
  • Union Types.
  • Type Aliases provide a easier way to manage types.
  • Interface declaration is another way to name an object type.
  • Using Type Assertion to specify a more specific type.
  • Provide Literal Types which is usful in Union types.
  • Add Enums feature.

Type Alias

With TypeScript, you can create complex types by combining simple ones. There are two popular ways to do so: with unions, and with generics. Types Alias make all these convenient to manage.

Describe Union

A popular use-case for union types is to describe the set of string or number literals that a value is allowed to be:

type WindowStates = "open" | "closed" | "minimized";
type LockStates = "locked" | "unlocked";
type PositiveOddNumbersUnderTen = 1 | 3 | 5 | 7 | 9;

Describe Tuple

type Address = [Number, String];
const address: Address = [1, "27 haha st"];

Describe Utility Types

interface Todo {
  title: string;
  description: string;
  completed: boolean;
  createdAt: number;
}
 
type TodoPreview = Omit<Todo, "description">;

Extract type from other

const project = {
  title: "project 1";
  specification: {
    areaSize: 100;
    rooms: 3;
  }
}
type Spec = typeof project["spectification"];

Interface

Interface is powerful in object. You can explicitly describe this object’s shape using an interface declaration.

Define an interface:

interface User {
  name: string;
  id: number;
}

Now, you can declare that a JavaScript object conforms to the shape of your new interface by using syntax like : TypeName after a variable declaration:

const user: User = {
  name: "Hayes",
  id: 0,
};

Since JavaScript supports classes and object-oriented programming, so does TypeScript. You can use an interface declaration with classes:

interface User {
  name: string;
  id: number;
}
 
class UserAccount {
  name: string;
  id: number;
 
  constructor(name: string, id: number) {
    this.name = name;
    this.id = id;
  }
}
 
const user: User = new UserAccount("Murphy", 1);

You can use interfaces to annotate parameters and return values to functions:

function deleteUser(user: User) {
  // ...
}
 
function getAdminUser(): User {
  //...
}

There is already 7 primitive types available in JavaScript which you can use in an interface. TypeScript extends this list with a few more, such as any (allow anything), unknown (ensure someone using this type declares what the type is), never (it’s not possible that this type could happen), and void (a function which returns undefined or has no return value).

interfaces vs type aliases

There are two main tools to declare the shape of an object: interfaces and type aliases.

They are very similar, and for the most common cases act the same. And because TypeScript is a structural type system, it’s possible to intermix their use.

Almost all features of an interface are available in type, the key distinction is that a type cannot be re-opened to add new properties vs an interface which is always extendable.

Only Type Alias can be used to alias a primitive. Interface only support object.

Type Alias is alias, and Interface is declaration.

Both support extending

Type aliases do this via intersection types, while interfaces have a keyword.

type BirdType = {
  wings: 2;
};

interface BirdInterface {
  wings: 2;
}
type Owl = { nocturnal: true } & BirdType;
type Robin = { nocturnal: false } & BirdInterface;

interface Peacock extends BirdType {
  colourful: true;
  flies: false;
}
interface Chicken extends BirdInterface {
  colourful: false;
  flies: false;
}

let owl: Owl = { wings: 2, nocturnal: true };
let chicken: Chicken = { wings: 2, colourful: false, flies: false };

But TypeScript recommend to use interfaces over type aliases. Specifically, because you will get better error messages.

interfaces are open and type aliases are closed.

This means you can extend an interface by declaring it a second time.

interface Kitten {
  purrs: boolean;
}

interface Kitten {
  colour: string;
}

For publicly exposed types, it’s a better call to make them an interface. But, I have to say, close is better than open.

Generics

  • Why Generics in TypeScript: TypeScripte copy this concept from Java and C#, which make it possible to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
  • Definiton: Generics take type(s) as input and use them to derive the type of variables or functions.
  • Purpose: Generics type give you chance to describe the type relationship between input and output.
  • Generics provide variables to types by using <> angle bracket.

Use Generics Examples

Array with Generic

A common example is an array. An array without generics could contain anything. An array with generics can describe the values that the array contains. For example:

type StringArray = Array<string>;
type NumberArray = Array<number>;
type ObjectWithNameArray = Array<{ name: string }>;

Promise with Generic

A Promise<number> represents a promise that will eventually return a number.

Custom Generic type

You can declare your own types that use generics:

interface Backpack<Type> {
  add: (obj: Type) => void;
  get: () => Type;
}

// This line is a shortcut to tell TypeScript there is a
// constant called `backpack`, and to not worry about where it came from.
declare const backpack: Backpack<string>;
 
// object is a string, because we declared it above as the variable part of Backpack.
const object = backpack.get();
 
// Since the backpack variable is a string, you can't pass a number to the add function.
backpack.add(23); // error: Argument of type 'number' is not assignable to parameter of type 'string'

What do <T extends XXX> means

This is used for constrain the generic type parameter. If you don’t explicitly constrain a generic type parameter via extends XXX, then it will implicitly be constrained by unknown.

<T extends Something> is indicate that the function can accept any parameter that extends Something.

Let’s say we have an interface Person and some other interfaces which implements Person like Teacher, Student. Now we have a function: function funcA<T extends Person>(t: T). And we can only call funcA with a parameter that extends T, that means Teacher or Student.

Understand Generics

From the initial purpose, making type as variable, making function accept type variable, we get Generic Function!

Generic Function

Generic function is a function which can take a type variable. Like this:

function identity<T>(arg: T): T {
  return arg;
}
/* Explicitly*/
identity<string>("Hello");
/* Implicitly */
identity("World") // the compiler just looked at the value "World", and set Type to its type.

The type of the Generic Function

What is The type of the Generic Function?

For Example:

function identity<Type>(arg: Type): Type {
  return arg;
}

let myIdentity: <Type>(arg: Type) => Type = identity;

From the code above, you can get that <Type>(arg: Type) => Type is the type of the Generic function.

We can also write the generic type as a call signature of an object literal type:

function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: { <Type>(arg: Type): Type } = identity;

We can wrap it up into a interface:

interface GenericIdentityFn {
  <Type>(arg: Type): Type;
}

Generic Types

(Generic Types usually refer to generic interface) Now, we may want to move the generic parameter to be a parameter of the whole interface. This lets us see what type(s) we’re generic over (e.g. Dictionary<string> rather than just Dictionary). This makes the type parameter visible to all the other members of the interface.

interface GenericIdentityFn<Type> {
  (arg: Type): Type;
}
 
function identity<Type>(arg: Type): Type {
  return arg;
}
 
let myIdentity: GenericIdentityFn<number> = identity;

Instead of describing a generic function, we now have a non-generic function signature ((arg: Type): Type) that is a part of a generic type (GenericIdentityFn<Type>).

When we use generic type GenericIdentityFn, we now will also need to specify the corresponding type argument (here: number), effectively locking in what the underlying call signature will use.

This generic type GenericIdentityFn is a interface, also called generic interface.

Understanding when to put the type parameter directly on the call signature and when to put it on the interface itself will be helpful in describing what aspects of a type are generic.

Generic Classes

A generic class has a similar shape to a generic interface.

class GenericNumber<NumType> {
  zeroValue: NumType;
  add: (x: NumType, y: NumType) => NumType;
}

Just as with interface, putting the type parameter on the class itself lets us make sure all of the properties of the class are working with the same type.

A class has two sides to its type: the static side and the instance side. Generic classes are only generic over their instance side rather than their static side, so when working with classes, static members can not use the class’s type parameter.

Understand Generic Constraints

Keep this in mind: The type in TypeScript is a tool that help you make your logic more organized. So Generic type provide constraints as well.

For example the code below, compiler will tell: Property ‘length’ does not exist on type ‘Type’.

function loggingIdentity<Type>(arg: Type): Type {
  console.log(arg.length); // Property 'length' does not exist on type 'Type'.
  return arg;
}

You can make your code more flexiable:

interface Lengthwise {
  length: number;
}

function loggingIdentity<Type extends Lengthwise>(arg: Type): Type {
  console.log(arg.length); // Now we know it has a .length property, so no more error
  return arg;
}

Now do this: loggingIdentity({ length: 10, value: 3 }); instead of loggingIdentity(3);.

We can Use Generic Constraints to make code more robust.

function getProperty<Type, Key extends keyof Type>(obj: Type, key: Key) {
  return obj[key];
}
 
let x = { a: 1, b: 2, c: 3, d: 4 };
 
getProperty(x, "a"); // OK
getProperty(x, "m"); // error: Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

Mixin

Mixins are a way to reuse a set of functions or properties in multiple classes, without using inheritance or composition. Mixins allow you to define a common behavior or functionality that can be shared by multiple classes, and they can be used to avoid duplication and to create more modular and flexible code.

To define a mixin in TypeScript, you can create a function or a class that contains the common behavior or functionality, and then use the mixin helper function to apply the mixin to one or more target classes. The mixin helper function creates a new class that combines the target class with the mixin, and it returns the resulting class.

Here is an example of using mixins in TypeScript:

function Timestamps<T extends new(...args: any[]) => {}>(Base: T) {
  return class extends Base {
    createdAt = new Date();
    updatedAt = new Date();
  };
}

class User {
  name: string;
  age: number;
}

const TimestampedUser = Timestamps(User);

const user = new TimestampedUser();
user.name = 'John';
user.age = 42;
console.log(user.createdAt);  // current date and time
console.log(user.updatedAt);  // current date and time

In this example, the Timestamps mixin is defined as a function that takes a Base class as a parameter, and it returns a new class that extends the Base class and adds the createdAt and updatedAt properties. The TimestampedUser class is created by applying the Timestamps mixin to the User class, and it combines the properties and methods of the User class with the createdAt and updatedAt properties of the mixin.

Mixins are a useful technique in TypeScript for reusing common behavior or functionality in multiple classes, and they can help to create more modular and flexible code. It is important to use mixins appropriately, as they can affect the inheritance hierarchy and the type compatibility of the code, and to make sure that the mixins do not interfere with the intended behavior of the code.

What do <T extends new(...args: any[]) => {}> mean

The main responsibility of the code is to declare that the type being passed in is a class. It basically saying that it need a constructor in the passing value’s definition. When you use Mixin, you will use this code a lot, cause Mixin focus on class.

Decorator

In TypeScript, decorators are a way to add additional behavior to classes, methods, properties, and other elements of the code.

Decorators are functions that are invoked with a special syntax, and they can be used to modify or extend the behavior of the decorated element in various ways.

Decorators are typically used to add metadata or to implement cross-cutting concerns, such as logging, validation, or error handling. They can be applied to classes, methods, properties, accessors, parameters, or even local variables, and they can be defined and used in different ways.

Here is an example of using a decorator in TypeScript:

function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function(...args: any[]) {
    console.log(`${propertyKey} method called with arguments: ${args}`);
    return originalMethod.apply(this, args);
  }

  return descriptor;
}

class MyClass {
  @log
  public method1(a: number, b: number): number {
    return a + b;
  }
}

const obj = new MyClass();
obj.method1(1, 2);  // logs: 'method1 method called with arguments: [1, 2]'

In this example, the log decorator is defined as a function that takes three parameters: the target object, the name of the decorated property, and the property descriptor. The decorator modifies the value property of the descriptor to wrap the original method in a logging function, and it returns the modified descriptor. The @log decorator is applied to the method1 method of the MyClass class, and it logs the arguments of the method whenever it is called.

Decorators are a powerful and flexible feature of TypeScript that can be used to add additional behavior to the code, and they can be useful for implementing cross-cutting concerns and for creating reusable and modular code. It is important to use decorators appropriately to ensure that they do not interfere with the intended behavior of the code, and to make the code easy to understand and maintain.

TypeScript Cheat Sheets

TypeScript Control Flow Analysis:

TypeScript Interfaces:

TypeScript Types:

TypeScript Classes:

FAQ

What is .d.ts files

.d.ts files are called type declaration files. They exist for one purpose only: to describe the shape of an existing module and they only contain type information used for type checking.

How do I assign a new property to an object in TypeScript?

Do this:

var obj: {[k: string]: any} = {};
obj.prop = "value";
obj.prop2 = 88;

Now using Record<Keys, Type> is a better way.

var obj: Record<string, unknown> = {};
obj.prop = "value";
obj.prop2 = 88;

never type?

What is never type

The never type is used to represent values that are never expected to occur or that are not intended to be used, such as the result of a function that always throws an exception or an infinite loop that never returns.

function fail(message: string): never {
    throw new Error(message);
}
function infiniteLoop(): never {
    while (true) {}
}
let x: never = fail('Something went wrong');  // ok
let y: never = infiniteLoop();  // ok
let z: number = x;  // error: Type 'never' is not assignable to type 'number'
let w: string = x;  // error: Type 'never' is not assignable to type 'string'

Why

The never type is used to indicate that a value or a function is not intended to be used or accessed. That can help to prevent errors and improve code quality.

unknown type?

What is unknown type

unknown is the type-safe counterpart of any.

Anything is assignable to unknown, but unknown is not assignable to anything but itself and any without a type assertion or a control flow based narrowing.

const foo: unknown = 1

console.log(foo > 10) // <- ⛔️⛔️⛔️ foo is still unknown, so it cannot be compared to a number
const myString: string = foo // <- ⛔️⛔️⛔️ unknown type is not assignable to string type

// Rest assure compiler, I know what I am doing with foo
console.log(foo as number > 10)  // This is OK

When to use unknown

Handle external data sources

How to use unknown

Using type assertion or a control flow based narrowing.

  • Using Typeguards
  • Using Type assertions

An alternative to typeguard is an assertion function to throw an error if the input does not conform to our Dog type:

type AssertDogFn = (value: unknown) => asserts value is Dog
const assertDog: AssertDogFn = (value) => {
  if(!value || typeof value !== 'object' || 'name' in value === false || (typeof (value as Dog).name !== 'string')) {
    throw new Error('error in parsing fetched value to Dog type')
  }
}
assertDog(fetchedDog)
const lowerCaseName = fetchedDog.name.toLowerCase() // ✅✅✅ <- all good, if fetchedDog is not a `Dog`, assertDog should have already thrown

What is extends keyword

extends keyword works differently according to contexts. Basically, we use it in three ways:

  • “Inheritance”. It is used for Interface Inheritance (or Class Inheritance).
  • “Generic Constraints”.
  • “Conditional Types”. TypeScript has the capability of making a logic to dynamically generate a type according to a type assigned to a generic.

What is Conditional Types?

A conditional type selects one of two possible types based on a condition expressed as a type relationship test: T extends U ? X : Y. The type above means when T is assignable to U the type is X, otherwise the type is Y.

type IsString<T> = T extends string ? true : false;

type x = IsString<'hello'>; // type x = true
type y = IsString<number>;  // type y = false
type z = IsString<string>;  // type z = true

How to Check the Type of an Object in Typescript?

  • Using the typeof Operator
  • Using the instanceof Operator
  • Using Type Guards
  • Using User-Defined Type Predicates

Typeguard

Typeguard is an expression — an additional check to assure TypeScript that an value conforms to a type definition.

The full name of Typeguard should be user-defined type guard functions.

Typeguard Syntax:

function isType(obj: any): obj is TypeName {
  // Type checking logic
}

It also called type predicates. You can think is is a keyword.

Example:

function isString(test: any): test is string{
    return typeof test === "string";
}

function example(foo: any){
    if(isString(foo)){
        console.log("it is a string" + foo);
        console.log(foo.length); // string function
    }
}
example("hello world");

Using the type predicate test is string in the above format (instead of just using boolean for the return type), after isString() is called, if the function returns true, TypeScript will narrow the type to string in any block guarded by a call to the function. The compiler will think that foo is string in the below-guarded block (and ONLY in the below-guarded block)

Anthoer Example:

type Dog = {name: string}
const fetchedDog: unknown = await fetchDogByIdFromApi(1)
const ourDog: Dog = fetchedDog //  ⛔️⛔️⛔️ <- unknown could not be assigned to Dog!

// Typeguards:
const isDog = (value: unknown): value is Dog => !!value && typeof value === 'object' && 'name' in value && typeof (value as Dog).name === 'string'

if(isDog(fetchedDog)) {
  const ourDogName = fetchedDog.name.toLowerCase() // ✅✅✅ <- all good, isDog is sufficient in making sure that fetchedDog has `Dog` type
} else {
  throw new Error('error in parsing fetched value to Dog type')
}

assertion function

An alternative to typeguard is an assertion function to throw an error if the input does not conform to our Dog type:

type AssertDogFn = (value: unknown) => asserts value is Dog
const assertDog: AssertDogFn = (value) => {
  if(!value || typeof value !== 'object' || 'name' in value === false || (typeof (value as Dog).name !== 'string')) {
    throw new Error('error in parsing fetched value to Dog type')
  }
}
assertDog(fetchedDog)
const lowerCaseName = fetchedDog.name.toLowerCase() // ✅✅✅ <- all good, if fetchedDog is not a `Dog`, assertDog should have already thrown

Using User-Defined Type Predicates

interface Dog {
    breed: string;
    bark: () => void;
}

interface Cat {
    breed: string;
    meow: () => void;
}

function isDog(obj: any): obj is Dog {
    return obj && typeof obj.bark === 'function';
}

const pet1: Dog = { breed: 'Labrador', bark: () => console.log('Woof!') };
const pet2: Cat = { breed: 'Siamese', meow: () => console.log('Meow!') };

console.log(isDog(pet1));
console.log(isDog(pet2));

What is Type assertion

Type assertion allows you to set the type of a value and tell the compiler not to infer it.

You may do this to make thing simple:

type ButtonColor = 'red'| 'blue' | 'green';

useEffect (()=>{
  const previousButtonColor = localStorage.getItem('buttonColor') as ButtonColor;
},[]);

How does the ‘===’ operator differ from ‘==’ in TypeScript?

The === operator checks for both value and type equality (strict equality), while the == operator only checks for value equality, performing type conversion if necessary.

What is the difference between ‘&&’ and ‘||’ operators in TypeScript?

The && operator (logical AND) returns the first falsy operand or the last operand if all are truthy. The || operator (logical OR) returns the first truthy operand or the last operand if all are falsy.

How do nullish coalescing (‘??’) and optional chaining (‘.?’) operators work in TypeScript?

The nullish coalescing operator (??) returns the right-hand operand if the left-hand operand is null or undefined. Optional chaining (?.) allows safe navigation of properties, returning undefined if any part of the chain is null or undefined.

What is the purpose of the spread (‘…’) operator in TypeScript?

The spread operator (…) is used to expand iterable elements like arrays, objects, or function arguments into individual elements or properties.

How does the conditional (ternary) operator work in TypeScript?

The conditional operator (condition ? expr1 : expr2) evaluates a condition and returns expr1 if true, otherwise returns expr2.

Reference