Type Guards and Differentiating Types

Union types are useful for modeling situations when values can overlap in the types can take on. What happens when we need to know specifically whether we have a Fish ? A common idiom in JavaScript to differentiate between two possible values is to check for the presence of a member. As we mentioned, you can only access members that are guaranteed to be in all the constituents of a union type.
// Try it out on TypeScript Playground interface Bird { fly(): void; layEggs(): void; } interface Fish { swim(): void; layEggs(): void; } declare function getSmallPet(): Fish | Bird; const pet = getSmallPet(); pet.layEggs(); // This condition will always return true since the function is always defined. Did you mean to call it instead?(2774) if( pet.layEggs ) { console.log( 1 ); } pet.layEggs(); // Property 'swim' does not exist on type 'Bird | Fish'. // Property 'swim' does not exist on type 'Bird'.(2339) pet.swim(); if( 'swim' in pet ) { pet.swim(); } // Property 'fly' does not exist on type 'Bird | Fish'. // Property 'fly' does not exist on type 'Fish'.(2339) if( pet.fly ) { // Property 'fly' does not exist on type 'Bird | Fish'. // Property 'fly' does not exist on type 'Fish'.(2339) pet.fly(); } const fishPet = pet as Fish; fishPet.swim(); if( fishPet.swim ) { fishPet.swim(); }

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.

Using type predicates

function isFish( pet: Fish | Bird ): pet is Fish { return ( pet as Fish ).swim !== undefined; }
export default ( x: unknown ): x is Error => ( {} ).toString.call( x ) === '[object Error]';

Using the in operator

if( 'swim' in pet ) { return pet.swim)(; }

typeof type guards

if( typeof x === 'number' ) { x.slice( 1 ); }

instanceof type guards

if( pet instanceof Birt ) { pet.fly(); }
The right side of the instanceof needs to be a constructor function, and TypeScript will narrow down to:
  1. the type of the function's prototype property if its type is not any .
  1. the union of types returned by that type's construct signatures.
in that order.

Nullable types

TypeScript has two special types, null and undefined, that have the values null and undefined respectively.
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 inventor of null, Tony Hoare, calls this his “billion dollar mistake”.
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:

!: identifier!

// Try it out on TypeScript Playground const user: { email?: string } = { email : 'a@b.com' }; let account, domain; // Object is possibly 'undefined'.(2532) [ account, domain ] = user.email.split( '@' ); [ account, domain ] = user.email?.split( '@' ) || []; [ account, domain ] = user.email!.split( '@' ) || [];

Interfaces vs. Type Aliaes

As we mentioned, type aliases can act sort of like interfaces; however, there are some subtle differences.
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.
Because an interface more closely maps how JavaScript objects 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.

Polymorphic this types

class Person { #name = ''; name( name: string ): this { this.#name = name; return this; } }

Mapped types

A common task is to take an existing type and make each of its properties optional:
type Person = { name: string; age: number; } // Type '{}' is missing the following properties from type 'Person': name, age(2739) const a: Person = {} const b: Partial<Person> = {}
To define another type which only makes properties optional except some specific properties.
type PartialExclude<T, M extends keyof T> = { [ P in keyof T ]?: T[ P ]; } & { [ P in M ]: T[ P ]; } // Type '{ age: number; }' is not assignable to type 'PartialExclude<Person, "name">'. // Property 'name' is missing in type '{ age: number; }' but required in type '{ name: string; }'.(2322) const c: PartialExclude<Person, 'name'> = { age : 123 }
type Partial<T> = { [ P in keyof T ]?: T[ P ]; } type Readonly<T> = { readonly [ P in keyof T ]: T[ P ]; } type Pick<T, K extends keyof T> = { [ P in K ]: T[ P ]; } type Record<K extends keyof any, T> = { [ P in K ]: T; }
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:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>;
Non-homomorphic types are essentially creating new properties, so they can't copy property modifiers from anywhere.

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.

Distributive conditional types

Conditional types in which the checked type is naked type parameter are called distributive conditional types . Distributive conditional types are automatically distributed over union types during instantiation. For example, an instantiation of T extends U ? X : Y with the type argument A | B | C for T is resolved a (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).
// Remove types from T that are assignable to U type Diff<T, U> = T extends U ? never : T; // Remove types from T that are not assignable to U type Filter<T, U> = T extends U ? T : never; // Remove null and undefined from T type NotNullable<T> = Diff<T, null | undefined>
type Diff<T, U> = T extends U ? never : T; type PartialWith<T, M extends keyof T> = { [ P in Diff<keyof T, M> ]: T[ P ]; } & { [ P in M ]?: T[ P ]; } const person: Person = { name : '', age : 1 } const a: PartialWith<Person, 'name'> = { age: 123 }

Type inference in conditional types

With in the extends clause of a conditional type, it is not possible to have infer declarations that introduce a type variable to be inferred. Such inferred type variables may be referenced in the true branch of the conditional type. It is possible to have multiple infer locations for the same type variable.
type ReturnType<T> = T extends ( ...args: any[] ) => infer R ? R : any;
type AllTypes<T> = T extends Records<keyof any, infer U> ? U : never; type A = { name: string; age: number; male: boolean; } // AA = string | number | boolean; type AA = AllTypes<A>;
When inferring from a type with multiple call signatures (such as the type of an overloaded function), inferences are made from last signature (which, presumably, is the most permissive catch-all case). It is not possible to perform overload resolution based on a list of argument types.
declare function foo( x: string ): number; declare function foo( x: number ): string; declare function foo( x: string | number ): string | number; // type T1 = string | number; type T1 = ReturnType<typeof foo>;