Skip to content

Typescript Advanced Concepts

Type Modifiers

any (Top type)

In type theory, a top type represents the type that is compatible with all other types. It is also known as the universal supertype or the any type. In other words, any value can be assigned to a variable of the top type. It is used to define a variable or parameter that can hold any type of value.

In TypeScript, the top type is represented by the any type. The any type can hold any value, and it disables the type checking for the variable. It is often used when the type of a variable is unknown or when interfacing with JavaScript code that does not have types.

unknown

In contrast to any, unknown is a type-safe counterpart of any that is designed to represent values that are not known at the time of compilation or that can come from various sources. Unlike any, unknown is a stricter type that cannot be assigned to any other type without first being checked or cast to a more specific type.

It is similar to the any type in that it allows any value to be assigned to it, but it provides additional type safety by not allowing the value to be used until it has been narrowed down to a more specific type.

  • TypeScript does not allow directly accessing properties of unknown typed values.
  • unknown is not assignable to types that are not a top type (any or unknown).

The only way TypeScript will allow code to access members on a name of type unknown is if the value’s type is narrowed, such as using instanceof or typeof, or with a type assertion.

never (Bottom type)

In type theory, a bottom type represents the type that is incompatible with all other types. It is also known as the universal subtype or the never type. In other words, no value can be assigned to a variable of the bottom type. It is used to define a function or expression that does not return a value or has an unreachable end point.

In TypeScript, the bottom type is represented by the never type. The never type is used to indicate that a function or expression will never return a value, or that it will always throw an error or enter an infinite loop. It is often used in type annotations for functions that throw exceptions or functions that have an unreachable end point.

Type Predicates

We can narrow types using JavaScript constructs such as instanceof, typeof and in. That’s all fine and good for directly using that limited set of checks, but it gets lost if you wrap the logic with a function.

Notice that it would work if we inlined typeof value === "number" || typeof value === "string";. It got lost because we wrap the logic with a function.

TypeScript has a special syntax for functions that return a boolean meant to indicate whether an argument is a particular type. This is referred to as a type predicate, also sometimes called a “user-defined type guard". You the developer are creating your own type guard like instanceof or typeof.

Type predicates are commonly used to indicate whether an argument passed in as a parameter is a more specific type than the parameter’s.

You can think of a type predicate as returning not just a boolean, but also an indication that the argument was that more specific type.

Type predicates are often used to check whether an object already known to be an instance of one interface is an instance of a more specific interface.

A bad usage example of type predicates.

Type predicates that do more than verify the type of a property or value are easy to misuse. Avoid them when possible. Simpler type predicates are sufficient for most cases.

Type Operators

Type operators in TypeScript are used to manipulate types and create new types based on existing types. Type operators are needed in TypeScript to allow for more advanced type manipulation. They provide a way to transform or extract types based on certain conditions or operations at compile-time.

keyof

The keyof operator is a type operator in TypeScript that is used to obtain the union type of all the property names of a given type.

In other words, if you have a type T with properties prop1, prop2, and prop3, then keyof T will give you the union type "prop1" | "prop2" | "prop3".

The keyof operator is useful because it allows us to create generic code that works with a variety of object types, without knowing the specific keys in advance. Instead of hardcoding specific property names in our code, we can use keyof to reference the keys dynamically. This makes our code more flexible, maintainable, and less error-prone.

In this example, TypeScript gives an error because we are attempting to access a property of the ratings object using a string value that may not be a valid property name. Since TypeScript is unable to determine the type of the value returned by ratings[key],

Another option would be to use a type union of literals for the allowed keys. That would be more accurate in properly restricting to only the keys that exist on the container value:

However, what if the interface has dozens or more members? You would have to type out each of those members’ keys into the union type and keep them up-to-date.

typeof

In TypeScript, the typeof operator is a type operator that returns the type of a given value or variable. It is used to obtain the static type of a value or variable at compile-time. It is a type-level operator that operates on values at compile-time rather than at runtime like the typeof operator in JavaScript.

Although the typeof type operator visually looks like the runtime typeof operator used to return a string description of a value’s type, the two are different. They only coincidentally use the same word. Remember: the JavaScript operator is a runtime operator that returns the string name of a type. The TypeScript version, because it’s a type operator, can only be used in types and won’t appear in compiled code.

keyof typeof

typeof retrieves the type of a value, and keyof retrieves the allowed keys on a type.

TypeScript allows the two keywords to be chained together. Putting them together keyof typeof type operator is used to get a union type of all the keys in a given type.

By combining keyof and typeof, we get to save ourselves the pain of writing out and having to update—types representing the allowed keys on objects that don’ t have an explicit interface type.

Const Assertions

In TypeScript, const assertions are a way to define a constant variable with a literal value that cannot be changed or narrowed by TypeScript's type system.

Const assertions can generally be used to indicate that any value—array, primitive, value, you name it—should be treated as the constant, immutable version of itself. Specifically, as const applies the following three rules to whatever type it receives:

  • Arrays are treated as readonly tuples, not mutable arrays.
  • Literals are treated as literals, not their general primitive equivalents.
  • Properties on objects are considered readonly.
  1. Read-Only tuples:
  1. Literals to primitives

It can be useful for the type system to understand a literal value to be that specific literal, rather than widening it to its general primitive.

It may also be useful to have specific fields on a value be more specific literals.

  1. Read-Only Objects

Object literals such as those used as the initial value of a variable generally widen the types of properties the same way the initial values of let variables widen. String values such as 'apple' become primitives such as string, arrays are typed as arrays instead of tuples, and so on. This can be inconvenient when some or all of those values are meant to later be used in a place that requires their specific literal type.

Asserting a value literal with as const, however, switches the inferred type to be as specific as possible. All member properties become readonly, literals are considered their own literal type instead of their general primitive type, arrays become read-only tuples, and so on. In other words, applying a const assertion to a value literal makes that value literal immutable and recursively applies the same const assertion logic to all its members.

Generics

In TypeScript, generics provide a way to create reusable components that can work with a variety of types, rather than being tied to a single type. They allow you to write code that is more generic and flexible, and can be used across a wide range of use cases.

A generic type is a type that takes one or more type parameters. These type parameters act as placeholders for types that will be supplied later, when the generic type is used.

A type parameter is just a placeholder for a type that can be used in a function or class definition. You can think of it as a variable that represents a type. The syntax for declaring a type parameter is to put the name of the parameter in angle brackets <> immediately before the function or class name, like this:

Just like regular parameters allow you to pass values to a function, type parameters allow you to pass types to a generic function. And just like regular parameters can have default values, type parameters can also have default types that are used when the type argument is not specified.

Generic Functions

In this example, identity is a function that takes a type parameter T. This means that the type of the argument and the return value will be the same as the type of the value that is passed in.

The following arrow function is functionally the same as the previous declaration:


The syntax for generic arrow functions has some restrictions in .tsx files, as it conflicts with JSX syntax.


Explicit Generic Call Types

When calling a generic function, TypeScript will usually try to infer the type arguments based on the type of the argument passed to the function. For example, if we pass an argument of type number to the identity function, TypeScript will infer the type argument for T as number.

However, there are cases when TypeScript cannot infer the type argument from the function call, especially when a generic construct is provided another generic construct whose type arguments are unknown. In such cases, TypeScript will default to assuming the unknown type for any type argument it cannot infer. This can lead to type errors if the inferred type argument is not compatible with the expected type.

To avoid defaulting to unknown, functions may be called with an explicit generic type argument that explicitly tells TypeScript what that type argument should be instead. TypeScript will perform type checking on the generic call to make sure the parameter being requested matches up to what’s provided as a type argument.

Generic Interfaces

Interfaces may be declared as generic as well. They follow similar generic rules to functions: they may have any number of type parameters declared between a < and > after their name. That generic type may later be used elsewhere in their declaration, such as on member types.

Inferred Generic Interface Types

As with generic functions, generic interface type arguments may be inferred from usage. When an interface with generic type parameters is used in a function or class, TypeScript can infer the type of the generic parameters based on the input arguments or the context in which the interface is used.

When we call boxValue with a string argument, TypeScript can infer that the type parameter T should be string, and therefore the return type of boxValue should be Box<string>. The inferred type of boxedString is Box<string>.

Generic Type Aliases

One last construct in TypeScript that can be made generic with type arguments is type aliases.

Generic Discriminated Unions

Generic Defaults

Sometimes, you might want to provide a default type for a generic parameter in case a specific type isn't provided. That's when generic defaults come into play.

Generic Constraints

Generic types by default can be given any type in the world: classes, interfaces, primitives, unions, you name it. However, some functions are only meant to work with a limited set of types.

You might want to make sure that the types being used as type arguments meet certain conditions or have specific properties or methods. This is where generic constraints come into play. You can use the extends keyword to specify a constraint for a generic type parameter.

Using Type Parameters in Generic Constraints (keyof)

Sometimes, you might want to reference other type parameters in your generic constraints. This allows you to create more dynamic and flexible constraints that depend on other type parameters.

Using extends and keyof together allows a type parameter to be constrained to the keys of a previous type parameter. It is also the only way to specify the key of a generic type.

Mapped Types

Mapped types iterate over the keys of an existing type, applying a transformation to each property. This allows you to create new types that are derived from other types, such as making all properties readonly, optional, or transforming their types.

Let's break down the syntax:

  1. {}: The curly braces are used to define an object type.
  2. [K in keyof OriginalType]: This part defines the iteration over the properties of OriginalType. keyof OriginalType returns a union of the property names (keys) of OriginalType. The K variable represents each property key in this iteration.
  3. NewProperty: This is the new property type that will be applied to each property of the input OriginalType. You can use the current property key K and the property type from OriginalType to define the transformed property type.

  1. Mapped types based on existing literals of unions
  1. Transforming Property Types:
  1. Changing Modifiers
  1. Removing modifiers
  1. Generic Mapped Types
  1. Key Remapping via as

In TypeScript 4.1 and later, you can use the as keyword in mapped types to perform key remapping. Key remapping allows you to change the keys of an object type during the mapping process.

  1. Mapped Types for Tuples

Mapped types are primarily designed for objects. However, you can still apply mapped types to tuples in some cases, as tuples are essentially objects with numeric keys.

  1. Variadic Tuple Types

A way to represent a tuple with a varying number of elements of different types. This feature allows you to express higher-order operations on tuples and arrays even when their lengths are unknown.

Conditional Types

TypeScript’s type system is an example of a logic programming language. It allows creating new constructs (types) based on logically checking previous types. It does so with the concept of a conditional type: a type that resolves to one of two possible types, based on an existing type.

Conditional type syntax looks like ternaries:

Let's break down the syntax:

  1. LeftType extends RightType: This is the condition we're checking. We want to see if the input type LeftType extends (or is assignable to) the type RightType.
  2. ? IfTrue : IfFalse: This is a ternary expression, similar to those found in many programming languages. If the condition is true (i.e., LeftType extends RightType), the resulting type will be IfTrue. If the condition is false, the resulting type will be IfFalse.

The logical check in a conditional type is always on whether the left type extends, or is assignable to, the right type.


  1. Generic Conditional Types
  1. Type Distributivity

Conditional types automatically distribute over union types. This means that if the checked type is a union, the conditional type will be applied to each member of the union separately:

  1. Inferred Types (infer)

The infer keyword in TypeScript is used specifically within the context of conditional types. It allows you to infer a type from another type within a conditional type expression. Essentially, it provides a way to capture and use the inferred type within the scope of the conditional type expression.

  1. T extends (infer E)[]: We check if T is an array type. If it is, we use the infer keyword to capture the type of the array elements and assign it to the type variable E.
  2. ? E : never: This is a ternary expression. If the condition is true (i.e., T is an array type), the resulting type will be E, which is the inferred element type. If the condition is false (i.e., T is not an array type), the resulting type will be never.
  1. T extends Promise<infer R>: We check if T is a Promise type. If it is, we use the infer keyword to capture the type of the resolved value and assign it to the type variable R.

  2. ? R : never: This is a ternary expression. If the condition is true (i.e., T is a Promise type), the resulting type will be R, which is the inferred resolved value type. If the condition is false (i.e., T is not a Promise type), the resulting type will be never.

  3. Mapped Conditional Types

Mapped types apply a change to every member of an existing type. Conditional types apply a change to a single existing type. Put together, they allow for applying conditional logic to each member of a generic template type.

Profile picture

I have a passion for all things web.