Classes

when comparing types that have private and protected members, we treat these types differently. For two types to be considered compatible, if one of them has a private member, then the other must have a private member that originated in the same declaration. The same applies to protected members.

TypeScript accessibility modifiers like public or private can’t be used on private fields.

When it comes to properties, TypeScript’s private modifiers are fully erased – that means that while the data will be there, nothing is encoded in your JavaScript output about how the property was declared. At runtime, it acts entirely like a normal property. That means that when using the private keyword, privacy is only enforced at compile-time/design-time, and for JavaScript consumers, it’s entirely intent-based.

Parameter properties

  1. class Octopus {
  2. readonly numberOfLegs: number = 8;
  3. constructor(readonly name: string) {}
  4. }
  5. let dad = new Octopus("Man with the 8 strong legs");
  6. dad.name;

Abstract class

Using a class as an interface

A class declaration creates two things: a type representing instances of the class and a constructor function. Because classes create types, you can use them in the same places you would be able to use interfaces.

Enums

Enums are one of the few features TypeScript has which is not a type-level extension of JavaScript. TypeScript provides both numeric and string-based enums.

While string enums don’t have auto-incrementing behavior, string enums have the benefit that they “serialize” well.

Technically enums can be mixed with string and numeric members, but it’s not clear why you would ever want to do so

Union enums and enum member types

Generics

Generic function

  1. interface GenericIdentityFn<T> {
  2. (arg: T): T;
  3. }
  4. function identity<T>(arg: T): T {
  5. return arg;
  6. }
  7. let myIdentity: GenericIdentityFn<number> = identity;

Generic types (interfaces)

  1. interface GenericIdentityFn {
  2. <T>(arg: T): T;
  3. }
  4. function identity<T>(arg: T): T {
  5. return arg;
  6. }
  7. let myIdentity: GenericIdentityFn = identity;

Generic Classes

As we covered in our section on classes, 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.

Generic Contrains

Advanced Types

Type Guards and Differentiating Types

As we mentioned, you can only access members that are guaranteed to be in all the constituents of a union type.

  1. let pet = getSmallPet();
  2. // You can use the 'in' operator to check
  3. if ("swim" in pet) {
  4. pet.swim();
  5. }
  6. // However, you cannot use property access
  7. if (pet.fly) {
  8. }

To get the same code working via property accessors, we’ll need to use a type assertion:

  1. let pet = getSmallPet();
  2. let fishPet = pet as Fish;
  3. let birdPet = pet as Bird;
  4. if (fishPet.swim) {
  5. fishPet.swim();
  6. } else if (birdPet.fly) {
  7. birdPet.fly();
  8. }

User-Defined Type Guards

It just so happens that TypeScript has something called a type guard. A type guard is some expression that performs a runtime check that guarantees the type in some scope.

To define a type guard, we simply need to define a function whose return type is a type predicate:

  1. function isFish(pet: Fish | Bird): pet is Fish {
  2. return (pet as Fish).swim !== undefined;
  3. }

pet is Fish is our type predicate in this example. A predicate takes the form parameterName is Type , where parameterName must be the name of a parameter from the current function signature.

Any time isFish is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.

  1. // Both calls to 'swim' and 'fly' are now okay.
  2. let pet = getSmallPet();
  3. if (isFish(pet)) {
  4. pet.swim();
  5. } else {
  6. pet.fly();
  7. }

Notice that TypeScript not only knows that pet is a Fish in the if branch; it also knows that in the else branch, you don’t have a Fish, so you must have a Bird.

Using the in operator

The in operator also acts as a narrowing expression for types.
For a n in x expression, where n is a string literal or string literal type and x is a union type, the “true” branch narrows to types which have an optional or required property n, and the “false” branch narrows to types which have an optional or missing property n.

  1. function move(pet: Fish | Bird) {
  2. if ("swim" in pet) {
  3. return pet.swim();
  4. }
  5. return pet.fly();
  6. }

typeof and instanceof type guards

  1. function padLeft(value: string, padding: string | number) {
  2. if (typeof padding === "number") {
  3. return Array(padding + 1).join(" ") + value;
  4. }
  5. if (typeof padding === "string") {
  6. return padding + value;
  7. }
  8. throw new Error(`Expected string or number, got '${padding}'.`);
  9. }

These typeof type guards are recognized in two different forms: typeof v === "typename" and typeof v !== "typename", where "typename" must be "number", "string", "boolean", or "symbol". While TypeScript won’t stop you from comparing to other strings, the language won’t recognize those expressions as type guards.

  1. interface Padder {
  2. getPaddingString(): string;
  3. }
  4. class SpaceRepeatingPadder implements Padder {
  5. constructor(private numSpaces: number) {}
  6. getPaddingString() {
  7. return Array(this.numSpaces + 1).join(" ");
  8. }
  9. }
  10. class StringPadder implements Padder {
  11. constructor(private value: string) {}
  12. getPaddingString() {
  13. return this.value;
  14. }
  15. }
  16. function getRandomPadder() {
  17. return Math.random() < 0.5
  18. ? new SpaceRepeatingPadder(4)
  19. : new StringPadder(" ");
  20. }
  21. let padder: Padder = getRandomPadder();
  22. // ^ = let padder: Padder
  23. if (padder instanceof SpaceRepeatingPadder) {
  24. padder;
  25. // ^ = Could not get LSP result: er;>
  26. < /
  27. }
  28. if (padder instanceof StringPadder) {
  29. padder;
  30. // ^ = Could not get LSP result: er;>
  31. < /
  32. }

The right side of the instanceof needs to be a constructor function, and TypeScript will narrow down to:

  • the type of the function’s prototype property if its type is not any
  • the union of types returned by that type’s construct signatures

in that order.

Nullable Types

By default, the type checker considers null and undefined assignable to anything. Effectively, null and undefined are valid values of every type. That means it’s not possible to stop them from being assigned to any type, even when you would like to prevent it.

The --strictNullChecks flag fixes this: when you declare a variable, it doesn’t automatically include null or undefined. You can include them explicitly using a union type

With --strictNullChecks , an optional parameter automatically adds | undefined:

  1. function f(x: number, y?: number) {
  2. return x + (y ?? 0);
  3. }
  4. f(1, 2);
  5. f(1);
  6. f(1, undefined); // ok
  7. f(1, null); // error

The same is true for optional properties.

In cases where the compiler can’t eliminate null or undefined, you can use the type assertion operator to manually remove them. The syntax is postfix !: identifier! removes null and undefined from the type of identifier

  1. interface UserAccount {
  2. id: number;
  3. email?: string;
  4. }
  5. const user = getUser("admin");
  6. user.id;
  7. Object is possibly 'undefined'.
  8. if (user) {
  9. user.email.length;
  10. Object is possibly 'undefined'.
  11. }
  12. // Instead if you are sure that these objects or fields exist, the
  13. // postfix ! lets you short circuit the nullability
  14. user!.email!.length;

Type Alias

Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types that you’d otherwise have to write by hand

Aliasing doesn’t actually create a new type - it creates a new name to refer to that type.

Just like interfaces, type aliases can also be generic - we can just add type parameters and use them on the right side of the alias declaration.

  1. type Container<T> = { value: T };

We can also have a type alias refer to itself in a property:

  1. type Tree<T> = {
  2. value: T;
  3. left?: Tree<T>;
  4. right?: Tree<T>;
  5. };
  6. type LinkedList<Type> = Type & { next: LinkedList<Type> };

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 a interface which is always extendable. As a workaround, you can extend a type via intersections.

  1. interface Window {
  2. title: string
  3. }
  4. interface Window {
  5. ts: import("typescript")
  6. }
  7. const src = 'const a = "Hello World"';
  8. window.ts.transpileModule(src, {});
  9. // a type can't be changed after created

Because an interface more closely maps how JavaScript object work by being open to extension, we recommend using an interface over a type alias when possible.

On the other hand, if you can’t express some shape with an interface and you need to use a union or tuple type, type aliases are usually the way to go.

Enum member types

As mentioned in our section on enums, enum members have types when every member is literal-initialized.

Much of the time when we talk about “singleton types”, we’re referring to both enum member types as well as numeric/string literal types, though many users will use “singleton types” and “literal types” interchangeably.

Polymorphic this types

A polymorphic this type represents a type that is the subtype of the containing class or interface.

Index Types

  1. function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
  2. return propertyNames.map((n) => o[n]);
  3. }
  4. interface Car {
  5. manufacturer: string;
  6. model: string;
  7. year: number;
  8. }
  9. let taxi: Car = {
  10. manufacturer: "Toyota",
  11. model: "Camry",
  12. year: 2014,
  13. };
  14. // Manufacturer and model are both of type string,
  15. // so we can pluck them both into a typed string array
  16. let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
  17. // If we try to pluck model and year, we get an
  18. // array of a union type: (string | number)[]
  19. let modelYear = pluck(taxi, ["model", "year"]);

The compiler checks that manufacturer and model are actually properties on Car. The example introduces a couple of new type operators. First is keyof T, the index type query operator. For any type T, keyof T is the union of known, public property names of T. For example:

  1. let carProps: keyof Car;
  2. // ^ = let carProps: "manufacturer" | "model" | "year"

The second operator is T[K], the indexed access operator.

Index types and index signatures

keyof and T[K] interact with index signatures. An index signature parameter type must be ‘string’ or ‘number’. If you have a type with a string index signature, keyof T will be string | number

  1. interface Dictionary<T> {
  2. [key: string]: T;
  3. }
  4. let keys: keyof Dictionary<number>;
  5. // ^ = let keys: string | number
  6. let value: Dictionary<number>["foo"];
  7. // ^ = let value: number

If you have a type with a number index signature, keyof T will just be number.

Mapped types

In a mapped type, the new type transforms each property in the old type in the same way.

  1. type Partial<T> = {
  2. [P in keyof T]?: T[P];
  3. };
  4. type Readonly<T> = {
  5. readonly [P in keyof T]: T[P];
  6. };

Note that this syntax describes a type rather than a member. If you want to add members, you can use an intersection type:

  1. // Use this:
  2. type PartialWithNewMember<T> = {
  3. [P in keyof T]?: T[P];
  4. } & { newMember: boolean }
  5. // This is an error!
  6. type WrongPartialWithNewMember<T> = {
  7. [P in keyof T]?: T[P];
  8. newMember: boolean;
  9. }

Let’s take a look at the simplest mapped type and its parts:

  1. type Keys = "option1" | "option2";
  2. type Flags = { [K in Keys]: boolean };
  3. // is equivalent to
  4. type Flags = {
  5. option1: boolean;
  6. option2: boolean;
  7. };
  1. type Nullable<T> = { [P in keyof T]: T[P] | null };
  2. type Partial<T> = { [P in keyof T]?: T[P] };

Note that Readonly<T> and Partial<T> are so useful, they are included in TypeScript’s standard library along with Pick and Record:

  1. type Pick<T, K extends keyof T> = {
  2. [P in K]: T[P];
  3. };
  4. type Record<K extends keyof any, T> = {
  5. [P in K]: T;
  6. };

Readonly, Partial and Pick are homomorphic whereas Record is not. One clue that Record is not homomorphic is that it doesn’t take an input type to copy properties from.
Note that keyof any represents the type of any value that can be used as an index to an object. In otherwords, keyof any is currently equal to string | number | symbol.

Inference from mapped types

Now that you know how to wrap the properties of a type, the next thing you’ll want to do is unwrap them. Fortunately, that’s pretty easy:

  1. function unproxify<T>(t: Proxify<T>): T {
  2. let result = {} as T;
  3. for (const k in t) {
  4. result[k] = t[k].get();
  5. }
  6. return result;
  7. }
  8. let originalProps = unproxify(proxyProps);
  9. // ^ = let originalProps: {
  10. rooms: number;
  11. }

Conditional Types

A conditional type selects one of two possible types based on a condition expressed as a type relationship test:

  1. T extends U ? X : Y

The type above means when T is assignable to U the type is X, otherwise the type is Y.

A conditional type T extends U ? X : Y is either resolved to X or Y, or deferred because the condition depends on one or more type variables. When T or U contains type variables, whether to resolve to X or Y, or to defer, is determined by whether or not the type system has enough information to conclude that T is always assignable to U.

Utility Types

Partial<Type>
Readonly<Type>
Record<Keys, Type>
Pick<Type, Keys>
Omit<Type, Keys>
Exclude<Type, ExcludedUnion>
Extract<Type, Union>
NonNullable<Type>
Parameters<Type>
ConstructorParameters<Type>
ReturnType<Type>
InstanceType<Type>
Required<Type>
ThisParameterType<Type>
OmitThisParameter<Type>
ThisType<Type>