TypeScript has gained staggering popularity in recent years, but why do we actually use it, what are its pros and cons?

Advantages of using TypeScript:

  • early detection of errors
  • predictability of operation thanks to static typing
  • code readability
  • better editor support during development
  • easier and faster refactoring
  • reduction of unit tests, especially those checking the correctness of structures

Disadvantages of using type checking:

  • additional time needed for code typing
  • limited trust due to the fact that in the end the code is compiled into dynamic JS
  • complete compiler, more often false negatives (compilation succeeds despite incorrect code) than false positives (no error, but compilation fails)
  • overhead on compilation time

TypeScript allows us to control the level of strictness. If we are in the process of implementing it in an existing project it is worth starting with a very loose configuration, while when starting with a new project it is best to configure it with the flags below:

  • strict
  • noUncheckedIndexAccess

What if the type is not known to us?

  • any type – you can do anything with it, no type checking, use of bar
  • unknown type – you can’t do anything with it unless you apply a true type check

Utility Types

An example of some often very helpful helper types available globally. You should read the entire list available in the documentation.

ReturnType
We get the type returned by the specified function type or function name:

ReturnType<FunctionType>
type T = ReturnType<() => string>; // string

ReturnType<typeof functionName>
declare function fn(): { a: number; b: string };
type T = ReturnType<typeof fn>; // { a: number; b: string }

Partial
We get a new type that has all the properties from the source type as optional:

Partial<T>
Partial<{name: string; point: number;}> // {name?: string; point?: number;}

Required
The resulting type contains all properties from the source type as required:

Required<T>
Required<{name?: string; point?: number;}> // {name: string; point: number;}

Readonly
We get a new type of which all properties from the source type become readonly:

Readonly<T>
Readonly<{name: string;}> // {readonly name: string;}

Pick
The new type created contains only the properties listed as the second parameter:

Pick<T, Keys>
Pick<{name: string; point: number;}, "name"> // {name: string;}

Omit
The new type is created from the source type, ignoring the properties listed in the second parameter:

Omit<T, Keys>
Omit<{name: string; point: number;}, "name"> // {point: number;}

NonNullable
Creates a new type by excluding null and undefined from the source type:

NonNullable<T>
NonNullable<string | number | undefined>; // string | number

Parameters
Extracts all function parameters as a type:

Parameters<T>
Parameters<(name: string, point: number) => void> // [name: string, point: number]

Dodatkowe ciekawostki:

Return this
It is possible to specify the returned type as this:

class ExampleClass {
  method = (): this => {
    return this;
  }
}

const assertions
Intentional narrowing of literals:

let x = "hello" as const; // '"hello"'
let y = [10, 20] as const; // 'readonly [10, 20]'
let z = { text: "hello" } as const; '{ readonly text: "hello" }'

Unions and intersections

Unions and intersections are some of the ways types are combined:

  • union: A | B – the result type contains the elements of both sets
  • intersection: A & B – the result type contains common elements of both sets

Discriminated Unions

If we are working with a type that consists of a union of subtypes, it is useful for each subtype to contain a property that defines the type. In the example below it is state, which allows us to implement various logic using switch or if when working with NetworkState type.

type NetworkLoadingState = {
  state: "loading";
};
type NetworkFailedState = {
  state: "failed";
  code: number;
};
type NetworkSuccessState = {
  state: "success";
  response: {
    title: string;
    duration: number;
    summary: string;
  };
};

type NetworkState =
 | NetworkLoadingState
 | NetworkFailedState
 | NetworkSuccessState;

Additionally, by implementing the Union Exhaustiveness check, we can be sure that we have handled every case:

function logger(s: NetworkState): string {
  switch (s.state) {
    case "loading":
      return "loading request";
    case "failed":
      return `failed with code ${s.code}`;
    case "success":
      return "got response";
    default:
      const _exhaustiveCheck: never = s;
      return s;
  }
}

In the above case, if the NetworkState type were extended with an additional subtype in the future, TypeScript would return an error telling us that the unsupported variant that falls into default is not of type never, so it cannot be assigned to the _exhaustiveCheck constant.

Typeguards

Ways to check the type of an object:

typeof

if (typeof arg === 'string') {
  // Within the block TypeScript knows that value must be a string
}

instanceof

if (arg instanceof ClassName) {
  // Within the block TypeScript knows that value must be an instance of ClassName
}

in

if (prop in object) {
  // Within the block TypeScript knows that property exists on an object
}

Literal type guard

if (s.state === "success") {
  // Within the block TypeScript can discriminate between union member
}

Custom type guard

function isClassName(arg: any): arg is ClassName {
  return (typeof arg.name === 'string');
}

Weak types

When an interface has all the fields of an optional type it is seen as a weak type. If we try to assign data to such a type that does not have a single property in common, TypeScript will return an error.

Generic constraints

We have the option of writing a generic function whose types will be constrained by us in some way.

function merge<U extends {length: number;}, V extends object>(obj1: U, obj2: V)
function prop<T, K extends keyof T>(obj: T, key: K)

Conditional Types

We can also use the ternary operator known from JavaScript, which allows us to create a specific type using a condition.

SomeType extends OtherType ? TrueType : FalseType;
type Example = Dog extends Animal ? number : string;

infer keyword

The infer keyword allows you to infer a type from another type within a conditional type.

type Foo<T> = T extends { a: infer U; b: infer U; } ? U : never;
Foo<{ a: string; b: string }>; // string
Foo<{ a: string; b: number }>; // string | number

type FlattenIfArray<T> = T extends (infer R)[] ? R : T
FlattenIfArray<string[]>; // string
FlattenIfArray<number>; // number

Ending

If you’d like to add anything from yourself, feel free to discuss in the comments 🙂