Table of contents
- What is Typescript?
- Why Typescript?
- Advanced Typescript cheat sheet
- Types vs interfaces in Typescript
- Type safety in Typescript - Unknown vs any
- The Differences Between Object, {}, and object in Typescript
- Using never type
- Using Enums
- Using Namespaces
- Using Typescript infer Like a Pro
- Using Typescript Conditional Types Like a Pro
- What are K, T, and V in Typescript Generics?
- Using Typescript Mapped Types Like a Pro
- Using Typescript Template Literal Types Like a Pro
- The Purpose of declare Keyword in Typescript
- How To Define Object Type With Unknown Structures in Typescript
- 5 Very Useful Tricks for Typescript Typeof Operator
- Typescript Visualized: 15 Most Used Utility Types
- Clean React With Typescript
- Using Typescript Type Predicates for Precise Type Checking
- Typescript Index Signature Explained
- Using Typescript Recursive Like a Pro
- Typescript Type Guards
- Conclusion
- References
Typescript is a widely used, open-source programming language perfect for modern development. With its advanced type system, Typescript allows developers to write more robust, maintainable, and scalable code. But, to truly harness the power of Typescript and build high-quality projects, it’s essential to understand and follow best practices. This article will walk through the Advanced Typescript concepts and capabilities. Whether you are just starting or you are an experienced Typescript developer, this article will provide valuable insights and tips to help you write clean, efficient code.
So, grab a cup of coffee, and let’s get started on our journey to mastering Typescript!
What is Typescript?
Typescript is a strongly typed programming language that builds on Javascript. It was originally designed by Anders Hejlsberg in 2012 and is currently developed and maintained by Microsoft as an open-source project.
Typescript complies with Javascript and can be executed in any javascript runtime (e.g. a browser or server Node.js).
Typescript supports multiple programming paradigms such as functional, imperative, and object-oriented. Typescript is neither an interpreted nor a complied language.
Why Typescript?
Typescript is a strongly typed language that helps prevent common programming mistakes and avoid certain kinds of run-time errors before programming is executed.
A strongly typed language allows the developer to specify various program constraints and behaviors in the data type definitions, facilitating the ability to verify the correctness of the software and prevent defects. This is especially valuable in large-scale applications.
Some of the benefits of Typescript:
Static typing, optionally strongly typed
Type inference
Access to ES6 and ES7 features
Cross-Platform and Cross-browser Compatibility
Tooling support with IntelliSense
Advanced Typescript cheat sheet
Typescript is a simple language and allows developers to express types in terms of other types. However, while performing the basic tasks, having a profound understanding of how Typescript works is critical for unlocking its advanced functionality.
As we learn more about Typescript, we can utilize this knowledge to write cleaner and testable code. In this section, we are combining all the basic concepts of Typescript and its advanced features in a single cheatsheet. so here we go.
Types vs interfaces in Typescript
We have two options for defining types in Typescript: types and interfaces. One of the most frequently asked questions about whether we should use interfaces or types.
The answer to this question, like many programming questions, is that it depends. In some cases, one has a clear advantage over the other, but in many cases, they are interchangeable.
Types and type aliases
type is a keyword in Typescript that we can use to define the shape of data. The basic types in Typescript include:
Each has unique features and purposes, allowing developers to choose the appropriate one for their particular use case.
Type aliases in Typescript mean “a name for any type“. They provide a way of creating new names for existing types. Type aliases don’t define new types; instead, they provide an alternative name for an existing type.
Type aliases can be created using the type keyword, referring to any valid Typescript type, including primitive types:
type MyNumber = number;
type User = {
id: number;
name: string;
email: string;
}
Interfaces in Typescript
In Typescript, an interface defines a contract to which an object must adhere. Below is an example:
interface Client {
name: string;
address: string;
}
Differences between types and interfaces
Primitive Types: Primitive types are inbuilt types in Typescripts. They include
number
,string
,boolean
,null
, andundefined
types. We can use define a type for a primitive type but we can’t use an interface to alias a primitive type.type Address = string; type NullOrUndefined = null | undefined;
Union types: Union types allow us to describe values that can be one of several types and create unions of various primitive, literal, or complex types. Union types can only be defined using type. There is no equivalent to a union type in an interface. But, it’s possible to create a new union type from two interfaces.
type Transport = 'Bus' | 'Car' | 'Bike' | 'Walk'; interface CarBattery { power: number; } interface Engine { type: string; } type HybridCar = Engine | CarBattery;
Function types: In Typescript, a function type represents a function’s type signature. Using a type alias, we need to specify the parameters and the return type to define a function type:
type AddFn = (num1: number, num2:number) => number; interface IAdd { (num1: number, num2:number): number; }
Both type and interface similarly define function types, except for a subtle syntax difference of interface using “:” vs “=>” when using type. Type is preferred because it’s short and thus easier to read.
Declaration merging: Declaration merging is a feature that is exclusive to interfaces. With declaration merging, we can define interfaces multiple times, and the Typescript compiler will automatically merge these definitions into a single interface definition.
In the following example: the two Client interface definitions are merged into one by the Typescript compiler, and we have two properties when using the Client Interface.
interface Client { name: string; } interface Client { age: number; } const harry: Client = { name: 'Harry', age: 41 }
Type aliases can’t be merged in the same way if you try to define the Client type more than once, as, in the above example, the error will be thrown.
When used in the right places, definition merging can be very useful. One common use case for declaration merging is to extend the third-party type definition to fit the particular project's needs.
If you need to merge declarations, interfaces are the way to go.
Extends vs. intersection: An interface can extend one or multiple interfaces. Using the
extend
keyword, an interface can inherit all the properties and methods of an existing interface while also adding new properties.For example, We can create a
VipClient
interface by extending theClient
interface:interface VIPClient extends Client { benefits: string[] }
To achieve a similar result for types, we need to use an intersection operator:
type VIPClient = Client & {benefits: string[]}; // Client is a type
You can extend an interface from a type alias with static known members:
type Client = { name: string; }; interface VIPClient extends Client { benefits: string[] }
Handling conflicts when extending: Another difference between types and interfaces is how conflicts are handled when you try to extend from one with the same property.
When extending interfaces, the same property isn’t allowed, as in the example below:
interface Person { getPermission: () => string; } interface Staff extends Person { getPermission: () => string[]; }
An error is thrown because a conflict is detected.
Type aliases handle conflicts differently. In the case of type aliases extending another type with the same property key, it will automatically merge all properties instead of throwing an error.
In the following example, the intersection operator merges the method signature of the two
getPermission
declarations and atypeof
operator is used to narrow down the union type parameter so that we can get the return value in a type-safe way:type Person = { getPermission: (id: string) => string; }; type Staff = Person & { getPermission: (id: string[]) => string[]; }; const AdminStaff: Staff = { getPermission: (id: string | string[]) =>{ return (typeof id === 'string'? 'admin' : ['admin']) as string[] & string; } }
It is important to note that the type intersection of two properties may produce unexpected results. In the example below, the
name
property for the extended typeStaff
becomesnever
, since it can’t be bothnumber
andstring
at the same time:type Person = { name: string }; type Staff = person & { name: number }; // error: Type 'string' is not assignable to type 'never'.(2322) const Harry: Staff = { name: 'Harry' };
Prefer extends over an intersection: Often, when using an interface, Typescript will generally do a better job displaying the shape of the interface in error messages, tooltips, and IDEs. It is also much easier to read, no matter how many types you combine or extend.
Compare that to the type alias that uses the intersection of two or more types like
type A = B & C;
, and you then type to use that alias in another intersection liketype X = A & D;
, TypeScript can struggle to display the structure of the combined type, making it harder to understand the shape of the type from the error messages.Typescript caches the results of the evaluated relationship between interfaces, like whether one interface extends another or if two interfaces are compatible. This approach improves the overall performance when the same relationship is referenced in the future.
In contrast, when working with intersections, TypeScript does not cache these relationships. Every time a type intersection is used, TypeScript has to re-evaluate the entire intersection which can lead to efficiency concerns.
For these reasons, it is advisable to use interface extends instead of relying on type intersections.
Advanced type features: TypeScript provides a wide range of advanced type features that can’t be found in interfaces. Some of the unique features in TypeScript include:
Type inferences: Can infer the type of variables and functions based on their usage. This reduces the amount of code and improves readability
Conditional types: Allow us to create complex type expressions with conditional behaviors that depend on other types
Type guards: Used to write sophisticated control flow based on the type of variable.
Mapped types: Transforms an existing object type into a new type
Utility types: A set of out-of-the-box utilities that help to manipulate types
TypeScript’s typing system constantly evolves with every new release, making it a complex and powerful toolbox. The impressive typing system is one of the main reasons many developers prefer to use TypeScript.
When to use types vs. interfaces
In many cases, they can be used interchangeably depending on personal preference. But we should use type aliases in the following use cases:
To create a new name for a primitive type
To define a union type, tuple type, function type, or another more complex type.
To overload functions.
To use mapped type, conditional types, type guards, or other advanced type features.
Compared with interfaces, types are more expressive. Many advanced type features are unavailable in interfaces, and those features continue to grow as Typescript is involved.
Type safety in Typescript - Unknown vs any
In Typescript, two types that are often used are any
vs unknown
. They might look the same at first, but they are used for different things.
Exploring unknown
The unknow
type is a safer choice than any
. When a variable is of type unknown, Typescript makes you check the type before you can use this variable. This makes developers clearly state their assumptions, which helps to avoid errors when the code is running.
Think about a situation when you are using the data from an API, and you are not sure what the data looks like. Using an unknown type helps you deal with uncertainty in a safe way.
function processData(data: unknown): string {
if (typeof data === 'string') {
return data.toUpperCase();
} else {
// Handle other cases appropriately
return "Invalid data";
}
}
// No compilation error
const result = processData("Hello, TypeScript!");
In simpler terms, using unknown
type ensures you double-check what kind of data you have before you do anything with it. This helps prevent mistakes that could happen when the program is running.
Exploring any
On the other hand, any
is the most easy-going type in Typescript. It skips type-checking, letting variables of this type be set to anything without making the code fail to compile. While this freedom can be handy, it means you lose the advantages of static typing.
Think about a situation where you want as much flexibility as possible, and you are sure that your code is type-safe.
function processDynamicData(data: any): string {
// No compilation error
return data.toUpperCase();
}
// No compilation error, works as expected
const result1 = processDynamicData("Hello, TypeScript!");
console.log(result1); // Outputs: "HELLO, TYPESCRIPT!"
// No compilation error, but will cause a runtime error
const result2 = processDynamicData(12345);
console.log(result2); // Error: data.toUpperCase is not a function
In simpler terms, using any means you don’t have to check the type of data, but it can cause problems if the data type is not what you expected.
When to use
When using Typescript, you can use unknown
when you want to be very careful with your code and make sure everything is the right type. any is used when you want to be more flexible, but this can make your code less safe. You should think carefully about which one to use, depending on what your code needs and how confident you are about the types and safety of your code.
The Differences Between Object
, {}
, and object
in Typescript
In Typescript, when we want to define an object type, there are several concise such as Object
, {}
, and object
. what are the differences between them?
Object (uppercased)
Object (uppercased) describes properties common to all Javascript objects. It is defined in the lib.es5.d.ts file that comes with the Typescript library.
As you can see, it includes some common properties like toString()
, valueOf()
, and so on.
Because it emphasizes only those properties that are common to Javascript objects. So you can assign boxable objects like string
, boolean
, number
, bigint
, symbol
to it, but not the other way around.
{}
{}
describes an object that has no members on its own, which means Typescript will complain if you try to access its property members.
From the code example above, we can see that {}
and Object
(uppercased) have the same features. That is, it can only access those properties that are common (even if the Javascript code logic is correct), and all boxable objects can be assigned to it, etc.
This is because the {}
type can access those common properties through the prototype chain, it also has no own properties. So it behaves the same as the Object
(uppercased) type. But they represent different concepts.
object (lowercased)
object
(lowercased) means any no-primitive type, which is expressed in code like this:
type PrimitiveType =
| undefined
| null
| string
| number
| boolean
| bigint
| symbol;
type NonPrimitiveType = object;
This means that all primitive types are not assignable to it, and vice versa:
Using never
type
In Typescript, never
is a special type that represents value that will never occur. It’s used to indicate that a function will not return normally, but will instead throw an error. This is a great way to identify to other developers and the compiler that a function can be used in certain ways, this can help to catch potential bugs.
For example, consider the following function that throws an error if the input is 0:
function divide(numerator: number, denominator: number): number {
if (denominator === 0) {
throw new Error("Cannot divide by zero");
}
return numerator / denominator;
}
Here, the function divide is declared to return a number, but if the denominator is zero, it will throw an error. To indicate that this function will not return normally in this case, you can use never
as a return type:
function divide(numerator: number, denominator: number): number | never {
if (denominator === 0) {
throw new Error("Cannot divide by zero");
}
return numerator / denominator;
}
Using Enums
Enums, short for enumerations, are a way to define a set of named constants in Typescript. They can be used to create a more readable and maintainable code, by giving a meaningful name to a set of related values.
For example, you can use an enum to define a set of possible status values for an order:
enum OrderStatus {
Pending,
Processing,
Shipped,
Delivered,
Cancelled
}
let orderStatus: OrderStatus = OrderStatus.Pending;
Enums can also have a custom set of numeric values or strings:
enum OrderStatus {
Pending = 1,
Processing = 2,
Shipped = 3,
Delivered = 4,
Cancelled = 5
}
let orderStatus: OrderStatus = OrderStatus.Pending;
Always name an enum with the first capital letter and the name has to be in singular form, as a part of the naming conversation.
Read this article to explore the cases of Typescript Enums that are used in the real world.
Using Namespaces
Namespaces are a way to organize your code and prevent naming collisions. They allow you to create a container for your code, where you can define variables, classes, functions, and interfaces.
For example, you can use a namespace to group all the code related to a specific feature:
namespace OrderModule {
export class Order { /* … */ }
export function cancelOrder(order: Order) { /* … */ }
export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);
You can also use namespaces to prevent naming collisions by providing a unique name for your code:
namespace MyCompany.MyModule {
export class MyClass { /* … */ }
}
let myClass = new MyCompany.MyModule.MyClass();
It’s important to note that namespaces are similar to modules, but they are used to organize the code and prevent naming collisions, while modules are used to load and execute the code.
Using Typescript infer Like a Pro
Do you know how to get the type of the elements in the T0 array type and the return value type in the T1 function type? Give yourself a few seconds to think about it.
type T0 = string[];
type T1 = () => string;
We can use the type pattern matching technology provided by Typescript - conditional types and infer to complete the previous requirements.
Conditional types allow us to detect the relationship between 2 types and determine whether they are compatible. Infer is used to declare a type variable to store the type captured during pattern matching.
Let’s see how to capture the type of the elements in the T0 array type:
type UnpackedArray<T> = T extends (infer U)[] ? U : T
type U0 = UnpackedArray<T0> // string
In the above code, T extends (infer U)[] ? U : T
is the syntax for conditional types, infer U
in the extended clauses introduces a new type variable U to store the inferred type.
For a better understanding, Let’s demonstrate the execution flow of the UnpackedArray
utility type.
It should be noted that infer can only be used in the extends clause of the conditional type, and the type variable declared by infer is only available in the true branch of the conditional type.
type Wrong1<T extends (infer U)[]> = T[0] // Error
type Wrong2<T> = (infer U)[] extends T ? U : T // Error
type Wrong3<T> = T extends (infer U)[] ? T : U // Error
For more use cases and more explanations, refer to this article.
Using Typescript Conditional Types Like a Pro
Have you used the Exclude, Extract, NonNullable, Parameters, and ReturnType utility types? Do you know how they work internally? In fact, the above Typescript built-in utility types are all developed based on Conditional Types.
Here we first briefly understand the specific implementation of these Typescript built-in utility types.
These utility types are used for the following purposes:
Exclude: Constructs a type by excluding it from
UnionType
all union members that are assigned toExcludedMembers
.Extract: Constructs a type by extracting from
Type
all union members that are assigned toUnion
.NonNullable: Constructs a type by excluding
null
andundefined
fromType
.Parameters: Constructs a tuple type from the types used in the parameters of a function type
Type
.ReturnType: Constructs a type consisting of the return type of the function
Type
.
Here we look at a few usage examples:
if you want to master them thoroughly and create you own utility types.
The built-in utility types described earlier use the Conditional Types introduced in Typescript 2.8 internally. The syntax for this type is as follows:
T extends U ? X : Y
So what are the uses of conditional types? Let’s take an example here:
type IsString<T> = T extends string ? true : false;
​
type I0 = IsString<number>; // false
type I1 = IsString<"abc">; // true
type I2 = IsString<any>; // boolean
type I3 = IsString<never>; // never
Refer this article to explore more.
What are K, T, and V in Typescript Generics?
Does it sound strange when you first see the T
in Typescript generics?
The T
in the figure is called a generic type parameters, and it is a type placeholder we wish to pass the identity function.
Just like passing parameters, we take the actual type specified by the user and chain it to the parameter type and return the value type.
So what does T means? The generic type parameter T in the figure represents Type, in fact, T can be replaced by any valid name. In addition to T, common generic variables are K, V, E, etc.
K(Key): represents a type of key in an object.
V(Value): represents a type of value in an object.
E(Element): represents the element type.
Of course, you don’t have to define only one type parameter, you can introduce any number of type parameters. Here we introduce a new parameter type U that extends the identity function we defined.
When calling the identity function, we can explicitly, specify the actual type of the generic parameter. Of course, you can also not specify the type of the generic parameter, and let Typescript automatically complete the type reference for us.
Using Typescript Mapped Types Like a Pro
Have you used the Partial, Required, Readonly, and Pick utility types?
Do you know how they work internally? if you want to master them thoroughly and create your own utility types, don’t miss the content below :)
User Registration is a widespread scenario in daily work. Here we can use Typescript to define User in which all keys are required.
type User = {
name: string;
password: string;
address: string;
phone: string;
};
Usually, for registered users, we allow users to modify only some user information. At this point, we can define a new UserPartial type representing the object type to update, in which all keys are optional.
type UserPartial = {
name?: string;
password?: string;
address?: string;
phone?: string;
};
For the scenario of viewing user information, we hope that all keys of the object type corresponding to the user object are read-only. For this requirement, we can define the Readonly User type.
type ReadonlyUser = {
readonly name: string;
readonly password: string;
readonly address: string;
readonly phone: string;
};
Reviewing the 3 user-related types already defined, we will see that they contain a lot of duplication code.
So how can we reduce those duplication codes in the above types? The answer is we can use the mapped type, which is a generic type that can be used to map the original object type to a new object type.
The syntax for mapped types is as follows:
Where P in K is similar to the Javascript for … in the statement, which is used to iterate through all types in type K, and the T type variable, which is used to represent any type in Typescript.
You can also use the additional modifiers read-only and question mark (?) in the mapping process. The corresponding modifiers are added and removed by adding the plus(+) and minus(-) prefixes. The default is to use the plus sign if no prefix is added.
We can now summarize the syntax of the common mapped type.
{ [ P in K ] : T }
{ [ P in K ] ?: T }
{ [ P in K ] -?: T }
{ readonly [ P in K ] : T }
{ readonly [ P in K ] ?: T }
{ -readonly [ P in K ] ?: T }
After introducing the syntax of common mapped types, let’s look at some examples:
Let’s look at how to redefine the PartialUser type using mapped types:
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
type UserPartial = MyPartial<User>;
In the above code, we define the MyPartial type mapped type and then use it to map the User type to the UserPartial type. The keyof
operator is used to get all the keys of a type, and its return type is a union type. The type variable P changes to a different type with each traversal, T[P], which is similar to the syntax for attribute access, and is used to get the type of the value corresponding to an attribute of the object type.
Let’s demonstrate the complete execution flow of MyPartial mapped types, if you are not sure, you can watch it several times to deepen your understanding of the Typescript mapped type.
Refer to this article to explore more.
Using Typescript Template Literal Types Like a Pro
When developing web pages, we usually use Tooltip or Popover to display prompt messages or explanatory information. In order to meet some usage scenarios, Tooltop or Popover will allow the user to set their placement position. For example, top, bottom, left, right, etc.
Since string literal types can basically spell-check our string values, we define a Side
type using Typescript’s type aliases.
type Side = 'top' | 'right' | 'bottom' | 'left';
let side: Side = "rigth"; // Error
// Type '"rigth"' is not assignable to type 'Side'.
// Did you mean '"right"'?ts(2820)
For the above 4 positions, it can already meet most scenarios. But if you want to set the placement position of the Tooltip more precisely, for example, let the Tooltip display in the upper area of the specified elements:
Then the existing Side
can’t meet the requirements, let’s define the new Placement
type:
type Placement = Side
| "left-start" | "left-end"
| "right-start" | "right-end"
| "top-start" | "top-end"
| "bottom-start" | "bottom-end"
In the Placement
type, in addition to the original 4 positions, we add 8 new positions such as “left-start“, “left-end“, and “right-start“, which correspond to the 8 string literal types. Looking at these string literal types, we find some duplicate code “-start“, and “-end“. In addition, when defining these literal types, spelling errors may occur if you are not careful.
So how can the above problems be solved better? This is where we can use the new template literal types introduced in Typescript 4.1, which is used in the following way:
type Alignment = 'start' | 'end';
type Side = 'top' | 'right' | 'bottom' | 'left';
type AlignedPlacement = `${Side}-${Alignment}`;
type Placement = Side | AlignedPlacement;
After reading the above code, do you think it is much simpler? Similar to template strings in Typescript, template literal types are enclosed in backticks and can contain placeholders of the form ${T}
. The actual type of the type variable T can be string, number, boolean, or bigint.
Template literal types provide us with the ability to concatenate string literals and convert literals of non-string primitive types to their corresponding string literal types. Here are some examples:
Refer to this article to explore more.
The Purpose of declare
Keyword in Typescript
When you open the *.d.ts file in Typescript projects, you may see delcare
. Do you know what delcare
does?
In Typescript projects, you may import third-party Js-SDK in the form of script tags, such as importing Js-SDK of the Google Maps platform.
<script
src="https://maps.googleapis.com/maps/api/js?key=AIzaSyB41DRUbKWJHPxaFjMAwdrzWzbVKartNGg&callback=initMap&v=weekly" defer></script>
After initialization, you will call the API exposed by the Js-SDK in a Typescript file.
Although you are using the API provided by Js-SDK according to the Google Maps development documentation, the Typescript compiler still prompts the corresponding error message for the above code. This is because the Typescript compiler does not recognize the global variable google
.
So how do we solve this problem? The answer is to use the declare keyword to declare the global google
variable so that the Typescript compiler can recognize the global variable.
declare var google: any;
Seeing this, do you get confused? Why can you use global variables like JSON
, Math
, or Object
in Typescript projects normally? This is because Typescript makes the declaration for us internally, and the global variables mentioned earlier are declared in the lib.es5.d.ts declaration file.
// typescript/lib/lib.es5.d.ts
declare var JSON: JSON;
declare var Math: Math;
declare var Object: ObjectConstructor;
In fact, in addition to declaring global variables, the declare keyword can also be used to declare global functions, global classes, and global enum types. Functions such as eval
, isNaN
, encodeURI
, and parseInt
that you may have used at work also declare in the lib.es5.d.ts declaration file.
declare function eval(x: string): any;
declare function isNaN(number: number): boolean;
declare function encodeURI(uri: string): string;
declare function parseInt(string: string, radix?: number): number;
It should be noted that when declaring a global function, we do not include the specific implementation of the function. With the declaration file, the Typescript can recognize the global Javascript functions.
Explore more here.
How To Define Object Type With Unknown Structures in Typescript
Did you encounter similar errors when you were learning Typescript?
To fix this error, use a very violent way is to use any
type:
let user: any = {}
user.id = "TS001";
user.name = "Bytefer";
Beside using any
type, how many solutions do you know? In this article, I will introduce 3 other solutions. Before you continue reading, I suggest you take a moment to think about it.
One of solutions is to use type
or interface
to define a User
type:
interface User {
id: string;
name: string;
}
let user = {} as User;
user.id = "TS001";
user.name = "Bytefer";
Although using the User
type, the previous problem can be solved. But you set a new age
property for the user object, the following error message will be displayed:
Property 'age' does not exist on type 'User'.ts(2339)
So how should we solve the problem of dynamic property assignment? At this point, we can use TypeScript’s index signatures. When we only know the type of the object keys and values, we can use index signatures to define the type of that object. The syntax of the index signatures is as follows:
The type of the key can only be string, number, symbol, or template literal type, while the type of the value can be any type.
The template literal types is a new type introduced in TypeScript 4.1, and in combination with index signatures, we can define more powerful types.
In addition to using index signatures, we can also use TypeScript’s built-in utility type Record
type to define the User
type. The role of the Record
utility type is as follows:
So what’s the difference between index signatures and a Record utility type? In some cases, they all define the expected type.
const user1: Record<string, string> = { name: "Bytefer" }; // Ok
const user2: { [key: string]: string } = { name: "Bytefer" }; // Ok
For index signatures, the key type can only be string, number, symbol, or template literal type. For the Record utility type, the key type can be a literal type or a union of literal types:
5 Very Useful Tricks for Typescript Typeof Operator
In Javascript, you can get the type of a variable through typeof
operator, do you know what the typeof
operator is used in Typescript? I will introduce 5 common application scenarios of the typeof
operator, which you may use in future projects.
Get the type of object
The man
object is a regular JavaScript object. In Typescript, you can use an interface or type to define the type of object. With this object type, you can use TypeScript’s built-in utility types, such as Partial, Required, Pick, or Readonly, to handle object types to meet different needs.
For simple objects, this may not be a big deal. But for large, complex objects with deeper nesting levels, manually defining their types can be mind-numbing. To solve this problem, you can use typeof
operator.
type Person = typeof man;
type Address = Person["address"];
Compared to manually defining the type before. It becomes much easier to use the typeof
operator. Person["address"]
is an indexed access type used to look up a specific property (address) on another type (Person type).
Get a Type That Represents All Enum Keys as Strings.
In Typescript, enum types are special types that get combined into regular Javascript objects.
Therefore, you can also use the typeof
operator on enum types. But this is often not of much practical use, and when dealing with enum types, it is usually combined with the keyof
operator.
Get The Type of The Function Object
There is another more common scenario in which the typeof operator is used in your work. After obtaining the corresponding function type, you can continue to use Typescript’s built-in ReturnType and Parameters utility types to obtain the function’s return value type and parameter type, respectively.
Get the Type of the Class Object
Since the typeof operator can handle the function object, can it handle Class objects? The answer is yes.
In the above code, createPoint
is a factory function that creates an instance of the Point class. Through the typeof operator, you can obtain the corresponding construct signature of the Point class, so as to realize the corresponding type verification. When defining the parameter type of Constructor, if the typeof operator is not used, the following error message will appear:
Get a More Precise Type
When using the typeof operator, if you want to get more a precise type, then you combine it with the cost assertion introduced in Typescript version 3.4. This is used in the following way.
As you see in the above figure, after using the cost assertion, and then using the typeof operator, you can obtain a more precise type.
Typescript Visualized: 15 Most Used Utility Types
In the process of using Typescript, we are programming type-oriented. In order to meet several work scenarios, we need to transform the known type. For the convenience of Typescript users, the Typescript team has provided us with many built-in utility types. With these utility types, we can easily convert types, extract types, exclude types, or get the parameter type or return type value of the function.
I picked 15 very useful utility built-in types and introduced their usage and internal working principles in the form of images or animations. After that, I believe you can really master the usage of these utility built-in types.
Partial<Type>
Constructs a type with all properties of Type
set to optional.
/**
* Make all properties in T optional.
* typescript/lib/lib.es5.d.ts
*/
type Partial<T> = {
[P in keyof T]?: T[P];
};
Required<Type>
Constructs a type consisting of all properties of Type
set to required. The opposite of Partial
.
/**
* Make all properties in T required.
* typescript/lib/lib.es5.d.ts
*/
type Required<T> = {
[P in keyof T]-?: T[P];
};
For more utility types, explore here.
Clean React With Typescript
Today, I use Typescript on a daily basis and actually like it quite a bit, even though I regularly break the compiler. One thing that took me quite some time though, was figuring out the best way to add Typescript to all the React patterns and features. I have seen it done in so many ways ending up confused by all the different types in the library itself.
To help you avoid that struggle or/and to improve your existing codebase, this article explores different use cases and - in my opinion - most elegant solutions.
Components
First of all, let’s talk about components.
They are the heart of every React application and probably the thing you write the most often. Even since the introduction of hooks, most components are plain Javascript functions that take a set of props and return some markup - usually in the form of JSX.
Given that, typing them is definitely straightforward. The only real constraint is that it always takes a single argument - an object of properties to be precise.
Basic Props
Let’s start with some primitive props first. In this example, we have a component that takes a title and a description. Both are string and description is optional.
type Props = {
title: string
description?: string
}
function ProductTile({ title, description }: Props) {
return (
<div>
<div>{title}</div>
{description && <div>{description</div>}
</div>
)
}
Children
So far, so good. Now a common use case is to pass children to a component in order to render nested components. You might be inclined to add it to your type, but React actually provides a special type that could help you reduce redundancy.
It’s called PropsWithChildren
and it’s a generic type that takes your existing props type and adds the children
for you.
import { ReactNode, PropsWithChildren } from 'react'
type Props = {
title: string
}
function ProductTile({ title, children }: PropsWithChildren<Props>) {
return (
<div>
<div>{title}</div>
{children}
</div>
)
}
Tip: This argument is optional. If your component only takes children, you can pass PropsWithChildren
on its own.
Piping Props
Another common thing to do is piping props in inner components. For example, you might have a component that itself renders our ProductTile
component, but also accepts additional props for customization.
import ProductTile from './ProductTile'
type Props = {
color: string
title: string
}
function ProminentProductTile({ color, title }: Props) {
return (
<div style={{ background: color }}>
<ProductTile title={title} />
</div>
)
}
While this is totally fine for primitive types such as strings or numbers, it can become cumbersome to repeat those types if you are dealing with complex records or functions - especially you have to pass value through multiple components.
Remember the DRY rule? Instead of repeating the same types over and over, we can leverage another generic provided by React which is ComponentProps
. It takes the type of a component that we can get by typeof
and returns its props type. we can then use indexed access to get the specific value.
import { ComponentProps } from 'react'
import ProductTile from './ProductTile'
type ProductTileProps = ComponentProps<typeof ProductTile>
type Props = {
color: string
title: ProductTileProps['title']
}
You can argue that we can achieve the same by simply exporting the type from ProductTile. And you are right, that works too, but it’s also more prone to errors if you change your types and/or extend them at some point.
Spreading Props
Similar to piping props, sometimes we want to spread all extra props to some underlying component. Imagine the ProminentProductTile
should pipe both title
and description
.
Instead of specifying each property one by one, we can simply extend the type we can leveraging ComponentProps
once again.
import { ComponentProps } from 'react'
import ProductTile from './ProductTile'
type Props = {
color: string
} & ComponentProps<typeof ProductTile>
function ProminentProductTile({ color, ...props }: Props) {
return (
<div style={{ background: color }}>
<ProductTile {...props} />
</div>
)
}
Tip: if you only want to spread a subset of those types, you can use a Typescript built-in Pick
to do so. For example:
type ProductTileProps = ComponentProps<typeof ProductTile>
type Props = {
color: string
} & Pick<ProductTileProps, 'title' | 'description'>
HTML elements
The same pattern also applies to HTML primitives. if you want to pass down all remaining props to e.g button, we can simply use ComponentProps<“button“>
.
Note: There's also React.JSX.IntrinsicElements["button"]
which refers to the identical type, but I would recommend you to use only one for consistency and readability and I generally prefer the first as it’s easier to use.
Extra: For certain edge cases where we only want to pass down valid HTML attributes - excluding React specific props such as ref and key - we can also use specific attribute types e.g. React.ButtonHTMLAttributes<HTMLButtonElement>
. However, I have not yet encountered a use case for those as it’s once again more to type, I still prefer the shorter ComponentProps<“button“>
.
Passing JSX
Now that we have covered simple props, let’s look at some advanced use cases. Sometimes, passing primitive props is not enough and we want to pass down the raw JSX to render nested content. While dependency injection generally makes your component less predictable, it’s a great way to customize generic components. This is especially useful, when we are already passing children, but need to inject additional makeup at a specific position.
Luckily, React provides us a another useful type called ReactNode
. Imagine a layout component that wraps your whole application and also receives the sidebar to render a dedicated navigation in a predefined slot.
import { PropsWithChildren, ReactNode } from 'react'
type Props = {
title: string
sidebar: ReactNode
}
function Layout({ title, children, sidebar }: PropsWithChildren<Props>) {
return (
<>
<div className="sidebar">{sidebar}</div>
<main className="content">
<h1>{title}</h1>
{children}
</main>
</>
)
}
// Now we can pass whatever we want
const sidebar = (
<div>
<a data-selected href="/shoes">
Shoes
</a>
<a href="/watches">Watches</a>
<a href="/shirts">Shirts</a>
</div>
)
const App = (
<Layout title="Running shoes" sidebar={sidebar}>
{/* Page content */}
</Layout>
)
Bonus: PropsWithChildren
is actually using ReactNode
under the hood. A custom implementation would look something like this:
type PropsWithChildren<Props = {}> = { children: ReactNode } & Props
Passing Components
Passing JSX is great when you want maximum flexibility. But what if you want to restrict rendering to certain components or pass some props to that subtree as well without moving all the logic into a single component bloating it even more?
What became popular as the render-prop pattern(new tab) is exactly that: dependency injection with constraints.
Once again, React has a neat type to help us with that called ComponentType
.
import { ComponentType } from 'react'
type ProductTileProps = {
title: string
description?: string
}
type Props = {
render: ComponentType<ProductTileProps>
}
function ProductTile({ render }: Props) {
// some logic to compute props
return render(props)
}
Extra: This is also quite nice for third-party components via d.ts files:
declare module 'some-lib' {
type Props = {
title: string
description?: string
}
export const ProductTile: ComponentType<Props>
}
Specific Components
If we only allow a specific component, the code is even simpler as we can use the built-in typeof
operator(new tab) to do so.
import { ComponentProps } from 'react'
import Icon from './Icon'
type Props = {
icon?: typeof Icon
} & ComponentProps<'button'>
function Button({ icon: Icon, children, ...props }: Props) {
return (
<button {...props}>
{icon && <Icon size={24} />}
{children}
</button>
)
}
Inferring Props
This is a common pattern used for generic (layout) components. It's often used in component libraries and can be a great foundation for creating flexible layouts in React.
Usually, you pass an as
or component
prop and the component becomes that, including its props and types. For example:
const App = (
<>
{/* this throws as div doesn't have a value prop */}
<Box as="div" value="foo" />
{/* this works however */}
<Box as="input" value="foo" />
</>
)
The best part is that it also works with custom components, giving us endless flexibility.
Alright, but how do we achieve this? Instead of explaining what Matt Pocock(new tab) from Total TypeScript(new tab) already did with a great article, I'm just going to link it here: Passing Any Component as a Prop and Inferring Its Props(new tab).
\=> For more cases of React in Typescript, explore them here.
Using Typescript Type Predicates for Precise Type Checking
Type predicates are a powerful tool used to check and ensure at runtime that a variable belongs to a specific type. By using type predicates, we can achieve more precise type checking when writing type-safe code, thereby avoiding type errors and enhancing the robustness and maintainability of the code.
Suppose we have a union type representing to animals, including cats(Cat) and dogs (Dog).
interface Cat {
kind: "cat";
meow: () => void;
}
interface Dog {
kind: "dog";
bark: () => void;
}
type Animal = Cat | Dog;
Now, we want to write a function to check whether an Animal
is of type Cat
. At this point, we can use the type predicate:
function isCat(animal: Animal): animal is Cat {
return animal.kind === "cat";
}
function makeSound(animal: Animal) {
if (isCat(animal)) {
animal.meow(); // Here, TypeScript knows that animal is of type Cat.
} else {
animal.bark(); // Here, TypeScript knows that animal is of type Dog.
}
}
In the example above, we define a function name isCat
and use the type predicate animal is Cat
as the return type. The function checks if the kind
property of the passed animal
object is equal to “cat”. If it is, the function returns to true and informs the Typescript compiler that within this conditional branch, the animal
variable is of type Cat
.
By doing this, the makeSound
function can accurately identify the specific type of Animal
and call the methods specific to Cat
or Dog
in the corresponding conditional branches. This not only enhances the type safety of the code but also makes the code cleaner and easier to maintain.
Typescript Index Signature Explained
An index signature is defined using square brackets []
and the type of keys, followed by the colon and the type for the corresponding values. It enables Typescript to understand and enforce the expected structure of the object.
interface MyStats {
[key: string]: number;
}
const scores: MyStats = {
total: 50,
average:80
}
// index siganture enforce the type constraint
// here, the value must be a number
const scores2: MyStats = {
total: "50", //Type 'string' is not assignable to type 'number'.(2322)
average:80
}
Note that index signatures can use different key types, such as string, number, symbol, or literal type and the associated value type can be any valid Typescript type.
Mixing an index signature with explicit members
In Typescript, we can mix an index signature with explicit member declarations. It’s helpful for cases requiring a combination of known and dynamic properties.
interface CarConfiguration {
[feature: string]: number;
price: number;
}
When we mix index signatures with explicit members, all explicit members need to conform to the index signature types.
// invalid case
interface CarConfiguration {
[feature: string]: number;
price: number;
model: string; // Error: Property 'model' of type 'string' is not assignable to 'string' index type 'number'
}
// valid
interface CarConfiguration {
[feature: string]: number | string;
price: number;
model: string;
}
Readonly index signature
Index signature supports readonly
modifier. By applying readonly
modifier, the properties in the object will be immutable.
interface Car {
readonly [key: string]: boolean;
}
const toyota: Car = {hybrid: true, luxury: false};
toyota.hybrid = false; //Error: Index signature in type 'Car' only permits reading.(2542)
In the above example, an error occurs when trying to modify hybrid
property because the interface only allow for reading, not writing.
How to use index signature
Let’s consider a real-world example of how index signatures can be used. Imagine you are developing an application with various features. Each feature includes its own set of setting, which can also enable or disable.
interface FeatureConfig {
[feature: string]: {
enabled: boolean;
settings: Record<string, boolean>;
}
}
In this example, we define an interface named FeatureConfig
. It uses an index signature to allow dynamic property names of type string
associated with anenabled
boolean property and a settings
object. It is handy for representing configurations with dynamic feature names and associated settings. For example, we can apply the interface to the following object.
const features: FeatureConfig = {
profile: {
enabled: true,
settings: {
showPhoto: true,
allowEdit: false,
},
},
notification: {
enabled: false,
settings: {
richText: true,
batchMode: true
},
}
};
In the features
object, the feature names can vary, but the structure for each feature remains consistent. Each feature is expected to have an enabled
boolean and a settings
object.
To improve the type safety, can we apply a union-type constraint to the feature
name in the above interface?
If the set of features in our application is known, we can define the union of string literals namedFeatureType
.
The key of the index signature does not support the union type, but we can work around it using a mapped type.
type FeatureType = 'profile' | 'notification' | 'reporting';
type FeatureConfig2 = {
[feature in FeatureType]: {
enabled: boolean;
settings: Record<string, boolean>;
}
}
[feature in FeatureType]
is a mapped type that iterates over each string literal in the union type FeatureType
(which includes 'profile'
, 'notification'
, and 'reporting'
), and it uses each value as the resulting type's property name.
Here’s an example of how we might use it:
const allFeatures: FeatureConfig2 = {
profile: {
enabled: true,
settings: {
showPhoto: true,
allowEdit: false,
},
},
notification: {
enabled: false,
settings: {
richText: true,
batchMode: true
},
},
reporting: {
enabled: true,
settings: {
template: false,
advanceExport: true
},
},
};
Note that we need to include all features defined in FeatureType
to the object to match the type expectations.
If we want to allow a subset of the features as the key, we need to modify the index signature type with an “?
” as an optional flag. Then, we could use the FeatureConfig2
type for an object that only contains a subset of features.
type FeatureType = 'profile' | 'notification' | 'reporting';
type FeatureConfig2 = {
[feature in FeatureType]?: {
enabled: boolean;
settings: Record<string, boolean>;
}
}
const subsetFeatures: FeatureConfig2 = {
profile: {
enabled: true,
settings: {
showPhoto: true,
allowEdit: false,
},
}
};
Index signature and Record type
A common question about index signature is “Why not use record type instead?“.
Record type defines an object type where we know the specific set of properties and their corresponding types. For example below, the UserRole
type restricts the keys to “admin”, “editor” or “viewer”.
type UserRole = "admin" | "editor" | "viewer";
type UserPermissions = Record<UserRole, boolean>;
const permissions: UserPermissions = {
admin: true,
editor: false,
viewer: true,
};
For index signature, we don’t know the specific properties; we only know the general type of property types. Thus, the use case of the index signature is to model dynamic objects, and the record type can be used for known object types.
Using Typescript Recursive Like a Pro
A linked list is a common data structure, that contains a series of nodes, each node contains two parts: data and a pointer to the next node. The first node in the linked list is called the head node and the last node is called the tail node.
So how do we define a linked table type in Typescript? if you want to use an type
or interface
to define a linked table type, we need to use the recursive type in Typescript. A recursive type is a type that can refer to itself. This type is useful when dealing with recursive structures because it allows us to define a type that can refer to itself to represent each node of a recursive structure.
Let’s take a look at how to use recursive types to define the type of singly linked list:
interface ListNode<T> {
data: T;
next: ListNode<T> | null;
}
interface LinkedList<T> {
head: ListNode<T> | null;
tail: ListNode<T> | null;
length: number;
get(index: number): ListNode<T> | null;
insert(index: number, value: T): boolean;
remove(index: number): ListNode<T> | null;
}
In the above code, ListNode<T>
represents the type of the chain node, and contains two properties, data
and next
, where the data
property is used to store the value of the node, and the next
property is used to store the pointer to the next node. Note that when the next property is not empty, it is of type ListNode<T>
, which uses Typescript’s recursive types. And LinkedList<T>
represents the type of a singly linked list, which contains the head node and the tail node, as well as the length property and some methods to manipulate the list.
For more details, refer to this article.
Typescript Type Guards
A type guard is an expression that performs a runtime check that guarantees the type in some scope. A typical application scenario for type guarding is to narrow the type scope of a union type. This is to ensure type safety, that is, to safely access specific properties or methods in a particular type of object during runtime.
typeof
type guards
First, let’s introduce the more common typeof
type of guards. The typeof
operator can obtain the type of an object at runtime, and the operator returns the following possible values.
"string"
"number"
"bigint"
"boolean"
"symbol"
"undefined"
"object"
"function"
So using the typeof
operator, we can get the actual of a variable at runtime. Let’s take an example:
function printId(id: string | number) {
if (typeof id === "string") {
console.log(`ID: ${id.toUpperCase()}`);
} else if (typeof id === "number") {
console.log(`ID: ${id}`);
}
}
So why use the typeof
operator to narrow down the type of the id
parameter? The main reason is to ensure the type safety at runtime. For example, when the type of id parameter is a numeric type, but we call the id.toUpperCase()
method, a runtime exception will be thrown.
In editors that support Typescript IntelliSense, you can access certain properties of the id parameter, you can access common properties of the string
and number
types. Specifically as shown in the picture below:
instanceof
type guards
Although the typeof operator can distinguish different types if we want to determine whether an object is an instance of a certain class, so as to safety access the unique properties or methods on the instance, then the typeof
operator can do nothing up. For this requirement, we can use instanceof
operator. Again, let’s take a concrete example:
class Shape {
constructor(public id: string) {}
}
class Circle extends Shape {
constructor(
public id: string,
public radius: number) {
super(id);
}
}
class Square extends Shape {
constructor(
public id: string,
public sideLength: number) {
super(id);
}
}
In the above code, we define a Shape
class and create two subclasses based on it. Next, we define a printShapeInfo
function to print information about different shapes:
function printShapeInfo(shape: Shape) {
if (shape instanceof Circle) {
console.log(`Circle's radius is: ${shape.radius}`);
} else if (shape instanceof Square) {
console.log(`Square's sideLength is: ${shape.sideLength}`);
}
}
in type guards
For the previous example of using the instanceof
operator to implement type guards, we can also use the form of interfaces to describe the Shape
, Circle
and Square
types.
interface Shape {
id: string;
}
interface Circle extends Shape {
radius: number;
}
interface Square extends Shape {
sideLength: number;
}
Because the type defined by the TypeScript interface does not generate the corresponding type after compilation, we cannot use the instanceof
operator for type detection at runtime. To realize the functions of the printShapeInfo
function, we can use the in
operator, the specific implementation is as follows:
function printShapeInfo(shape: Shape) {
if ("radius" in shape) {
console.log(`Circle's radius is: ${shape.radius}`);
} else if ("sideLength" in shape) {
console.log(`Square's sideLength is: ${shape.sideLength}`);
}
}
user-defined type guards
To demonstrate user-defined type guards, let’s redefine the 3 types:
interface Shape {
id: string;
}
interface Circle extends Shape {
radius: number;
}
interface Square extends Shape {
sideLength: number;
}
After defining the types related to Shape
, let’s define the user-defined type guards function:
function isCircle(shape: Shape): shape is Circle {
return "radius" in shape;
}
function isSquare(shape: Shape): shape is Square {
return "sideLength" in shape;
}
Compared with ordinary functions, custom type guard functions return type predicates. shape is Circle
in the above code is the so-called type predicate. A predicate takes the form parameterName is Type
, where parameterName
must be the name of a parameter from the current function signature. You can understand the role of the isCircle
user-defined type guard function in this way. If the return value of the function is true
, the type of the shape
parameter is the Circle
type.
Now that we have the isCircle
and isSquare
functions, we can use them in the printShapeInfo
function like this:
function printShapeInfo(shape: Shape) {
if (isCircle(shape)) {
console.log(`Circle's radius is: ${shape.radius}`);
} else if (isSquare(shape)) {
console.log(`Square's sideLength is: ${shape.sideLength}`);
}
}
equality narrowing type guards
In addition to the 4 type guarding methods described earlier, TypeScript also supports the use of if/switch
statements and equality checks, such as, ===
, !===
, ==
and !=
operators to narrow the types of variables.
function printValues(a: string | number, b: string | string[]) {
if (a === b) {
console.log(a.toUpperCase()); // (parameter) a: string
console.log(b.toUpperCase()); // (parameter) b: string
} else {
console.log(a); // (parameter) a: string | number
console.log(b); // (parameter) b: string | string[]
}
}
In the above code, printValues
function supports a
and b
2 parameters, and their types are union types. When the a===b
expression evaluates to true
, the types of the parameters a
and b
will be narrowed to string
type. Of course, using the !==
operator can also be used to achieve type narrowing.
function printValues2(a: string | number, b: string | string[]) {
if (a !== b) {
console.log(a); // (parameter) a: string | number
console.log(b); // (parameter) b: string | string[]
} else {
console.log(a.toLowerCase()); // (parameter) a: string
console.log(b.toLowerCase()); // (parameter) b: string
}
}
Conclusion
I hope that you have found this article helpful and that it has inspired you to become a better Typescript developer.
Happy coding ❤️
References
https://blog.logrocket.com/types-vs-interfaces-typescript
https://dev.to/sachinchaurasiya/type-safety-in-typescript-unknown-vs-any-55c0?context=digest
https://dev.to/zacharylee/the-differences-between-object-and-object-in-typescript-f6f?context=digest
https://itnext.io/mastering-typescript-21-best-practices-for-improved-code-quality-2f7615e1fdc3
https://programming.earthonline.us/what-are-k-t-and-v-in-typescript-generics-9fabe1d0f0f3
https://programming.earthonline.us/with-these-articles-you-will-not-be-confused-when-learning-typescript-d96a5c99e229#16ee
https://javascript.plainenglish.io/how-to-make-your-typescript-code-more-elegant-73645401b9b1
https://levelup.gitconnected.com/typescript-index-signature-explained-b040a78a0467
https://levelup.gitconnected.com/typescript-type-guards-in-6-minutes-9a9bab7fbe78