TypeScript gains more and more popularity among the Javascript developers, becoming even a stardard when it comes to nowadays software development and replacing to some extent Javascript itself.
Desipte the main goal of this language is to provide type-safety programing in to the chaotic Javascript, many people use it just because that’s the trend. In that case the only feature they use is revealing the types of given values, and if they can’t type something, an any
is being used instantly.
Well… TypeScript is so much more. It provides many features, so let’s focus on the ones that will help you with type organizing as well as bringing more security to your code.
A brief story of any
If you used TypeScript, it’s likely that you have been using any
so much. This type is quite uncertain one and can mean... everything.. literally everything.
When you type something by any
is same as you would say “I don’t care about what the type is here”, so you essentially ignore the typing here as if you were use plain Javascript.
For that reason any
should not (almost) never been used, because you ignore the typing, the thing that TypeScript was actually built for!
You may raise a question “ok, but what if I totally don’t know what the type is?!”. Yeah, in some cases you really don’t know it, and for that is better to use unknow
over the any
.
The unknow
type is very similar to any
- also match to everything, except one thing - is type-safe. Considering an example:
let x: unknown = 5;
let y: any = 5;
// Type 'unknown' is not assignable to type 'string'.
let x1: string = x;
// fine.
let y1: string = y;
As you can see, when you use unknown
in the context of string, the TypeScript doesn’t allow me to do this, because they are different types, while with any
I can do whatever I want.
That’s why any
is very insecure. Using any
makes your code prone to even crash as you are using one data in the context of different.
Does it mean I can’t use any
? No, any
has its own purpose, I will show you later. In terms of typing function arguments, return values, type aliases etc. - stay with unknown
.
Protection with type guards
This is really important feature of TypeScript. It allows you to check types in your code to assure that your data-flow relies on the correct data types. Many people use it, without even knowing that it’s named “type guards”. Let’s go with examples.
function product(x: number) {}
function discount(x: string) {}
function cart(x: string | number) {
// Argument of type 'string | number' is not assignable to parameter of type 'number'.
product(x);
// Argument of type 'string | number' is not assignable to parameter of type 'number'.
discount(x);
}
What’s happening here? We have function cart
that takes one argument which can be either string
or number
. Then we call two functions, each requires also one argument, first (product
) number
second (discount
) string
. For both functions, the argument from cart has been used - why does TypeScript raise an error?
Well, TypeScript basically has no clue what you want to do. We are giving string
or number
then use it in a different contexts - once just number
then just string
. What if you pass string to the function product
? Is that correct? Obviously not - it requires a different type. The same with function discount. That’s the issue here.
We must sift somehow possible types, to make sure, we have the right one in the given context. This is the goal of type guards - we make protection in given line against passing incorrect types.
typeof checking
In this particular case, a typeof
guard is completely enough:
function cart(x: string | number) {
if (typeof x === 'number') {
product(x);
}
if (typeof x === 'string') {
discount(x);
}
}
Now, everything receives the correct types. Worth notice, if we put return statement inside of first “if” then second if is no longer needed! TypeScript will catch the only one possibility is there.
The object complexity
How about more complex types? What if we have something more sophisticated than primitives?
type Product = {
qty: number;
price: number;
}
type Discount = {
amount: number;
}
function product(x: Product) {}
function discount(x: Discount) {}
function cart(x: Product | Discount) {
// Argument of type 'Product | Discount' is not assignable to parameter of type 'Product'.
product(x);
// Argument of type 'Product | Discount' is not assignable to parameter of type 'Product'.
discount(x);
}
We have here the same scenario as in the previous example, but this time we have used more complex types. How to narrow down them?
To distinguish "which is which" we can use in
operator and check whether the certain fields are present in the object.
For instance, our Product
has price
while the Discount
has amount
- we can use it as differentiator.
function cart(x: Product | Discount) {
if ('price' in x) {
product(x);
}
if ('amount' in x) {
discount(x);
}
}
Now, again TypeScript is satisfied, however, is that clean enough?
Customized type guards
A previous solution may solve the problem and works pretty well… as long as you not emerge more complex types - having sophisticated in
clause won’t be so meaningful - so what can we do?
TypeScript provides a is
operator that allows you to implement special kind of function you can use as type guard.
function isProduct(x: Product | Discount): x is Product {
return 'price' in x;
}
function isDiscount(x: Product | Discount): x is Discount {
return 'amount' in x;
}
function cart(x: Product | Discount) {
if (isProduct(x)) {
product(x);
}
if (isDiscount(x)) {
discount(x);
}
}
Look at the example above. We could create a checker-functions that bring capability to confirm the input type is what we expect.
We use statement of is
to define, a function which returns boolean
value that holds the information if the given argument acts as our type or not.
By using customised type-guards, we can also test them separately and our code becomes more clear and readable.
The configuration is tough…
Agree. The configuration of TypeScript is also quite complex. The amount of available options in a tsconfig.json
is overwhelming.
However there are bunch of them that commits to the good practices and quality of the produced code:
- *strict *- strict mode, I would say that’s supposed to be oblibatory always, it forces to type everything
- *noImplicitAny *- by default, if there is no value specified, the
any
type is assigned, This option forces you to type those places and not leaveany
(eg. function arguments) - *strictNullChecks *- the
null
andundefined
are different values, you should keep that in mind, so this option strictly checks this - *strictFunctionTypes *- more accurate type checking when it comes to function typings
Obviously there are more, but I think those ones are the most important in terms of type-checking.
More types? Too complex.
Once you project grow, you can reach vast amount of types. Essentially, there is nothing bad with that, except cases when one type was created as copy of the other one just because you needed small changes.
type User = {
username: string;
password: string;
}
// the same type but with readonly params
type ReadOnlyUser = {
readonly username: string;
readonly password: string;
}
Those cases break the DRY policy as you are repeating the code you have created. So is there any different way? Yes - mapping types.
The mapping types are build for creating new types from the existing ones. They are like regular functions where you take the input argument and produce a value, but in the declarative way: a function is generic type and its param is a function param. Everything that you assign to that type is a value:
type User = {
username: string;
password: string;
}
// T is an "argument" here
type ReadOnly<T> = {
readonly [K in keyof T]: T[K]
}
type ReadOnlyUser = ReadOnly<User>
In the example above, we created a mapping type ReadOnly
that takes any type as argument and produces the same type, but each property becomes readonly. In the standard library of TypeScript we can find utilities which are built in exactly that way - using mapping types.
In order to better understand the mapping types, we need to define operations that you can do on types.
keyof
When you use a keyof
it actually means “give me a union of types of the object key’s”. For more detailed information i refer to the official documentation, but for the matter of mapping types when we call:
[K in keyof T]
We access the “keys” in the object T
, where each key stays under the parameter K
- Sort of iteration, but in the declarative way as K
keeps the (union) type of keys, not a single value.
As next, knowing that K
has types of each parameter in a given object, accessing it by T[K]
seems to be correct as we access the “value” that lives under the given key, where this key also comes from the same object. Connecting those statements together:
[K in keyof T]: T[K]
We can define it: “go over the parameters of the given object T
, access and return the value that type T
holds under given key”. Now we can do anything we want with it - add readonly, remove readonly, add optional, remove optional and more.
The “if” statements
Let’s assume other example:
type Product = {
name: string;
price: number;
version: number; // make string
versionType: number; // make string
qty: number;
}
// ???
type VersioningString<T> = T;
type X = VersioningString<Product>
We have type Product
and we want to create another type that will change some properties to string, let's say the ones related to version: version
and versionType
.
We know how to “iterate” but we don’t know how to “make a if”.
type VersioningString<T> = {
[K in keyof T]: K extends "version" | "versionType" ? string : T[K]
};
We can put the “if” statements in that way by using extend keyword. Since that’s declarative programming, we operate on the types we are checking if our K
type extends… the union type of “version” and “versionType” - makes sense? In this meaning we check the inheritance of given type, just like among the classes in oriented programming.
Type inferencing
TypeScript always tries to reveal the types automatically and we can access it and take advantage of revealed type.
It’s quite handy when when it comes to matching something by extend
keyword and obtain the inferenced type at the same time.
type ReturnValue<X> = X extends (...args: any) => infer X ? X : never;
type X1 = ReturnValue<(a: number, b: string) => string> // string
This is classical example of obtaining the return type of given function. As you can see, by using extend
we can check whether input arg (generic) is a function by its signature, but in that signature we also use infer
keyword to obtain of what return type is, then save it under the X
field.
Connecting all of pieces together - A real world scenario
Using those mechanics, let’s now break down the following type:
type CartModel = {
priceTotal: number;
addToCart: (id: number) => void
removeFromCart: (id: number) => void
version: number;
versionType: number;
}
Our goal is to create a new type that, skips fields related to versioning and adds quantity argument to both addToCart
and removeFromCart
methods. How?
Since there is no simple declarative operations of skipping fields, we need to implement it in the other way. We know that it's feasible to create a new type from existing one by going over the fields of it, however we exactly want to limit those fields.
type SingleCart <T> = {
// keyof T ??
[K in keyof T]: T[K]
}
// prints all fields as normal
type Result = SingleCart<CartModel>
// goal:
type SingleCart <T> = {
[K in <LIMITED VERSION OF FIELDS OF T>]: T[K]
}
How can we achieve that? Normally to access all of the fields we use keyof T
but we our goal is to limit te list of possible keys of T.
Since the keyof T
gives us a union of the fields, we can limit this by using extend keyword:
// access all of the fields
type R1 = keyof CartModel
type SkipVersioning <T> = T extends "version" | "versionType" ? never : T
// gives union of "priceTotal" | "addToCart" | "removeFromCart"
type R2 = SkipVersioning<keyof CartModel>
So now we can use that type:
type SkipVersioning <T> = T extends "version" | "versionType" ? never : T
type SingleCart <T> = {
[K in SkipVersioning<keyof T>]: T[K]
}
/*
it gives a type:
type ResultType = {
priceTotal: number;
addToCart: (id: number) => void;
removeFromCart: (id: number) => void;
}
*/
type Result = SingleCart<CartModel>
We have just removed fields related to the version!
The next part is adding a quantity
argument to functions in the type. As we already have access to the type of given field (T[K]
), we need to introduce another one for transforming if given type is function:
type AddQuantityToFn<T> = ... // ??
type SingleCart <T> = {
[K in SkipVersioning<keyof T>]: AddQuantityToFn<T[K]>
}
The T[K]
is being wrapped by a new type AddQuantityToFn
. This type needs to check whether given type is a function and if that's true, add to this function a new argument quantity
if not, don't do anything. The implementation may look as follows:
type AddQuantityToFn <T> = T extends (...args: infer A) => void ?
(quantity: number, ...args: A) => void
:
T
If the type is a function (extends (...args: infer A) => void
), add a new argument quantity: number
(returns a type of (quantity: number, ...args: A) => void
) if not, keep the old type T
. Please notice we are using also type inferencing (infer A
) to grab the old argument's types.
Below, full implementation of it:
// Skips properties by given union
type SkipVersioning <T> = T extends "version" | "versionType" ? never : T
// Adds new argument to the function
type AddQuantityToFn <T> = T extends (...args: infer A) => void ?
(quantity: number, ...args: A) => void
: T
// Goes over the keys (without versioning ones) and adds arguments of quantity if that's method.
type SingleCart <T> = {
[K in SkipVersioning<keyof T>]: AddQuantityToFn<T[K]>
}
type ResultType = SingleCart<CartModel>
Quick summary: First of, we have defined a type that generates for us a union of property names besides ones related to versioning. Secondly, type for creating a new argument - if the type if function - if not, return given type. Lastly, our final type that goes over the keys (filtered) of an object, and adds arguments to the method (if needed).
Recap
TypeScript might be difficult and helpful at the same time. The most important thing is to start using types in a wise way with an understanding of how they work and with a right configuration that will lead you to produce properly typed code.
If that's something overwhelming for newcomers, would be nice to introduce it gradually and carefully and in each iteration provide better and better typings as well as type guarding of your conditional statements.
Top comments (2)
thanks so much!
Very helpful post