Photo by Anthony Intraversato on Unsplash
Typescript for frontend developers
Typescript comes with a severe learning curve and no doubt for beginners.
Table of contents
- Remember major Types in typescript
- What is type noImplicitAny
- What is type unknown
- Union and intersection types
- How to use Keyof in typescript
- How Typeof in Typescript is different
- ReturnType in typescript
- What are Generics in Typescript
- Using Conditional types in Typescript
- Using typeof, and keyof types together
- How the TypeScript Record Type Works
- Closing words
Hey developers! I know you are working on an exciting project. I assume that, while starting the project, a person from management asked you to use Typescript and you are aware of TS. A year ago when Typescript was an immense fear for me, I never thought I will be using it in my every project and loving it. Typescript is an excellent language that has many cool features like
Catches errors at compile-time.
Provides documentation.
Allows editors to flag errors while typing and provides completion.
Makes refactoring less error-prone.
Makes some tests unnecessary.
Disallows many JavaScript-type coercions.
Very helpful in frontend frameworks like Vue and React, obviously in angular too
Typescript comes with a serious learning curve, and no doubt for beginners, it can give you a tough time but you have to learn it and grasp its advanced concepts because now the industry demands this technology almost in all new projects. But stay with me, because I am going to tell you some senior typescript concepts, I know you‘ll be loving them. Let’s go developers.
Remember major Types in typescript
"Hello"
is a string2
is a numbertrue/false
is a boolean{}
is an object[]
is an arrayNaN
is not a number, not basically a real type.null
is empty or unknownundefined
means no value is set
credit: Conrad Davis Jr. on medium
What is type noImplicitAny
Typescript’s default behavior is to set the default type to function, or variable and mostly it types any
for variables. Here is the function;
function testijgNoImplicitAny(args) {
return args;
}
So here typescript will infer type automatically. Typescript gives you control over types. If making a variable typed is mandatory then there is an option to tell the compiler to produce an error on untyped code, so here is how to do this
Add "noImplicitAny": true
{
"compilerOptions": {
"target": "esnext",
"noImplicitAny": true -> here is the line
}
underlined error in red color - very nice it is working
To remove this error we can use any type such as any
, number, string
What is type unknown
The unknown
type is similar to any
type in that all types are assignable to any
, and unknown
type.
Assignments to and from
unknown
require a type check, whereas assignments to and fromany
do not.The
unknown
is a more restrictive type thanany
, meaning that fewer operations are allowed on values of typeunknown
without a type check.unknown
can be narrowed to a more specific type using type guards, whereasany
cannot.
function example1(arg: any) {
const a: str = arg; // no error
const b: num = arg; // no error
}
function example2(arg: unknown) {
const a: str = arg; // 🔴 Type 'unknown' is not assignable to type 'string'.(2322)
const b: num = arg; // 🔴 Type 'unknown' is not assignable to type 'number'.(2322)
}
any
type is bidirectional, whereas unknown
is unidirectional.
Union and intersection types
An intersection type is a type that combines several types into one; a value that can be any one of several types is a union type. The & symbol is used to create an intersection, whereas the | symbol is used to represent a union.
Union types
Are joining multiple types with the "pipe" (|
) symbol. For example, the type string | number
is a union type that can be either a string or a number.
let value: string | number;
value = "hello";
console.log(value.toUpperCase()); // okay, value is of type 'string'
value = 42;
console.log(value.toFixed(2)); // Error: value.toFixed is not a function
Intersection types
Are joining multiple types with the "ampersand" (&
) symbol. For example, the type string & number
is an intersection type meaning one variable is a string and the other is a number.
interface A {
x: number;
}
interface B {
y: string;
}
let value: A & B;
value = { x: 1, y: "hi, ts developers" }; // okay
console.log(value.x); // 1
console.log(value.y); // "hi, ts developers"
Using Union and Intersection types together rock, for example
interface Person {
name: string;
age: number;
}
interface Employee {
employeeId: number;
salary: number;
}
interface Student {
studentId: number;
major: string;
}
type PersonType = Person & (Employee | Student);
let person: PersonType;
person = {
name: "John",
age: 25,
employeeId: 123,
salary: 50000
};
console.log(typeof person.name, person.name); // string John
console.log(typeof person.employeeId, person.employeeId) ; // number 123
How to use Keyof
in typescript
In TypeScript, the keyof
operator is used to create a new type that represents the keys of a given object type. The new type is a union of the string literals of the object's property names. To be honest, since I knew this type I am learning to use it. I mostly use it with Typescript record types.
A bit of usage of keyof
interface Person {
name: string;
age: number;
}
type PersonKeys = keyof Person;
let key: PersonKeys;
key = "name"; // okay
key = "age"; // okay
key = "address"; // Error: "address" is not a property of type 'PersonKeys'
PersonKeys
is a union of the string literals of the property names of Person
, only the values "name"
and "age"
are valid values for key
.
Use-case of using keyof
interface Car {
make: string;
model: string;
year: number;
}
const myCar: Car = {
make: "Toyota",
model: "Camry",
year: 2020
};
function updateCar(
car: Car,
prop: keyof Car,
value: string | number
) {
car[prop] = value;
}
// 👍 okay
updateCar(myCar, "make", "Honda");
// 👍 okay
updateCar(myCar, "year", 2022);
// 👎 Error: "color" is not a property of type 'keyof Car'
updateCar(myCar, "color", "red");
// 👎 Error: "year" is not a property of type 'string'
updateCar(myCar, "year", "2022");
There is an interface Car
that defines a car with three properties: make
, model
, and year
. There is an object named myCar
interfaced Car
.
The updateCar
function takes three arguments:
car
of typeCar
.prop
of typekeyof Car
.value
of typestring | number
which can bestring
ornumber
based on the prop.
The function modifies the value of the property identified by prop
to value
.
How Typeof in Typescript is different
JavaScript already has a typeof
operator you can use in an expression context:
// Prints "string"
console.log(typeof "Hello world");
TypeScript adds a
typeof
operator you can use in a type context to refer to the type of a variable or property.
let text: string;
const firstName: typeof text;
const lasrName: typeof text;
firstName, lastName
are a variable whose type is inferred using the typeof
operator and the value of text
. Because text
is of type string
.
ReturnType in typescript
The ReturnType
utility type in TypeScript is used to extract the return type of a function or method.
function add(a: number, b: number): number {
return a + b;
}
let addReturnType: ReturnType<typeof add>;
add
is a function that takes two numbers as arguments and returns a number.
addReturnType
is a variable whose type is inferred using the ReturnType
utility type and the function add
. And ReturnType<typeof add>
is the same as number
.
Use-case of ReturnType
Here is a real-world example using array functions.
data
const data = [
{ id: 1, name: 'John' }, // id -> number, name -> string
{ id: 2, name: 'Jane' }
];
implementing some array functions
const findById = (id: number) => data.find(item => item.id === id);
const getName = (item: any) => item.name;
const toUpperCase = (str: string) => str.toUpperCase();
extracting ReturnType
and storing them in variables
let foundItem: ReturnType<typeof findById> = findById(1);
console.log(foundItem); // { id: 1, name: 'John' } // { id: number, name: string }
let itemName: ReturnType<typeof getName> = getName(foundItem);
console.log(itemName); // 'John' // string
let upperCasedName: ReturnType<typeof toUpperCase> = toUpperCase(itemName);
console.log(upperCasedName); // 'JOHN' // string
What are Generics in Typescript
As per typescript docs,
Being able to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.
When you create a generic component, you can specify a placeholder type (also called a type parameter) that represents the actual type that the component will work with. Mostly this parameter is set to T like <T> and these angle brackets are known as type-interface
.
Syntax of Generics
A simple generic function that takes an array of elements and returns the first element of the array:
function syntax<T>(arg: T): T {
return arg;
}
T
is the type parameter that represents the actual type of elements in the array. When you call the syntax
function, you can specify the type of elements by providing a type argument. So in simple words, arguments will decide the final type.
function getFirst<T>(arr: T[]): T {
return arr[0];
}
let numbers = [1, 2, 3];
let firstNumber = getFirst<number>(numbers); // 1
let strings = ['a', 'b', 'c'];
let firstString = getFirst<string>(strings); // 'a'
type of the elements in the array is inferred from the array argument.
Using Conditional types in Typescript
The conditional ternary operator is a very well-known operator in Javascript. The ternary operator takes three operands. A condition, a return type if the condition is true, and a return type is false.
syntax
condition ? returnTypeIfTrue : returnTypeIfFalse;
Before going forward a bit of note about extending types
<aside> 💡 In TypeScript, you can use the extends
keyword to create new types that are based on existing types.
interface Person {
name: string;
age: number;
}
interface Employee extends Person {
salary: number;
}
Here is how to use conditional types by leveraging generics <T>
.
interface IdWithString {
id: string;
}
interface IdWithNumber {
id: number;
}
type Id<T> = T extends string ? IdWithString : IdWithNumber;
let idA: Id<string> = { id: "stringId" };
// Type is inferred as IdWithString
let idB: Id<number> = { id: 1 };
// Type is inferred as IdWithNumber
The Id
type is a generic type that takes a type parameter T
, and it uses a conditional type to check if T
is assignable to string
Using typeof, and keyof types together
It is rocking to use keyof
and typeof
together in TypeScript. The keyof
keyword allows you to extract the keys of an object type, while typeof
allows you to extract the type of a variable.
so here is how
const colors = {
red: '#ff0000',
green: '#00ff00',
blue: '#0000ff',
}; // 1
type Colors = keyof typeof colors; // 2, note
let selectedColor: Colors = 'green'; // 3
let anotherSelection: Colors = 'yellow'; // 4
console.log(colors[selectedColor]); // 5
// docs
/**
1. Type '"yellow"' is not assignable to type '"red" | "green" | "blue"'
2. trying to get types of properties
3. yeah, it is correct
4. Type '"yellow"' is not assignable to type '"red" | "green" | "blue"'
5. '#00ff00'
note: keyof typeof colors is equal to "red" | "green" | "blue"
*/
How the TypeScript Record Type Works
The Record
type is used to create an object type that has a set of properties with a specific key type and a specific value type. For a beginner, it is a bit tough to understand types per data type.
When I was young in typescript I usually use an empty object {}
as a type of object, then IDE told me, dude! there is a Record type, use that.
Syntax of Record type
Record<K extends keyof any, T>
Where K
is the type of the keys of the object and T
is the type of the values of the object.
type TRecords = Record<string, number>;
let tryingRecords: TRecords = {
"key1": 1,
"key2": 2,
};
tryingRecords
is an object type that has properties with keys of type string
and values of type number
.
Using typescript Records and union types together
A practical example of how you can use Record
and union types together in TypeScript:
interface User {
id: number;
name: string;
}
interface Admin {
id: number;
name: string;
permissions: string[];
}
interface Guest {
id: number;
name: string;
expiration: string;
}
type UserType = "user" | "admin" | "guest";
type Users = Record<UserType, User | Admin | Guest>;
let users: Users = {
user: { id: 1, name: "John" },
admin: { id: 2, name: "Jane", permissions: ["read", "write"] },
guest: { id: 3, name: "Jim", expiration: "2022-12-31" }
};
There are three interfaces that represent different types of users (
User
,Admin
, andGuest
),There is a union type
UserType
to combine three types into a single type that can be one of"user"
,"admin"
or"guest"
.There is an object with
Record
type to define a new typeUsers
. TypeUsers
is an object type with properties that have keys that can only be the string literals"user"
,"admin"
, and"guest"
,values of type
User | Admin | Guest
which is a union of all the three interfaces.
Using record types with typeof
, and keyof
without interfaces
But there is a better way to type objects without even the use of interfaces, and I name it time-saving types.
data
// Data
const productInventory = {
shirts: {
small: 10,
medium: 15,
large: 20
},
pants: {
small: 5,
medium: 10,
large: 15
},
shoes: {
size7: 25,
size8: 30,
size9: 35
}
};
logic
type Product = keyof typeof productInventory;
type ProductSizes = typeof productInventory[Product];
function checkInventory(product: Product, size: keyof ProductSizes): boolean {
return productInventory[product][size] > 0;
}
console.log(checkInventory("shirts", "medium")); // true
console.log(checkInventory("pants", "small")); // true
console.log(checkInventory("shoes", "size9")); // true
console.log(checkInventory("pants", "extra large")); //compile error, extra large is not key of ProductSizes
productInventory
→ information about different products and the sizes available for each product,Record
type to define a new typeProductSizes
ProductSizes
is an object type with properties that have keys of theproductInventory[Product]
object and values of type number.Product
is the keyofproductInventory
to reference the specific product.ProductSizes
is the type ofproductInventory[Product]
to reference the specific product sizes.
Closing words
For front-end developers, TypeScript can provide several benefits to improve the development experience. TypeScript can help catch errors early by providing better type safety. which can make your code more robust and maintainable.
Using TypeScript in frontend development can also help you take advantage of modern JavaScript features such as JSX, async/await, and destructuring, while still providing the benefits of static type checking. This can make it easier to write and maintain complex and large-scale applications.