I often see that JS devs struggle to create good types in Typescript. Some people use the famous any
and other use unspecific types.
First of all, I'd like to remark that good typing helps you to think less and reduce to time to check the implementation. In Functional Programming, the function definition is so important for the same reason. Your types should be thrust and strictly define which is the structure of your data.
Today, we'll explore some tips about how to use some utils types and some extra cases that will help you on a daily basis.
Pick and Omit
These two utils are part of the utils that comes with Typescript. These are helpful for preventing rewrite interfaces every time that we need something similar. Let's see in action in a real example.
Imagine that we are creating a store to be used in some components.
interface Stores {
user: User,
theme: Theme
// ... some others
}
If we want to define the props of our component that also comes with some of these stores we don't need to replicate it like that:
interface AvatarProps {
user: User,
rounded: boolean
// ... some others
}
Instead, we could use these utils types to prevent repeat these types, and reduce some mistakes like add another type for the user prop.
interface AvatarProps extends Pick<Stores, "user">{
rounded: boolean
// ... some others
}
Pick
util just create a new type only with the keys that match with the second type that we passed. Imagine this like a function with 2 parameters, the first one is the whole type and the second one is a union with the names that we need to "pick". Remember a union is a conjunction of 2 or more types, in this case, we use a fixed string to match with each key.
interface Foo {
key1: number,
key2: number,
key3: number
}
type FooPicked = Pick<Foo , "key1" | "key2">
/*
This will result in a type like that:
interface FooPicked {
key1: number,
key2: number
}
*/
Omit
util does the same thing but in inverse order. I mean instead of taking every key that matches with the union it'll "omit" every key that matches with the union.
interface Foo {
key1: number,
key2: number,
key3: number
}
type FooOmited = Omit<Foo , "key1" | "key2">
/*
This will result in a type like that:
interface FooOmited {
key3: number
}
*/
Partial
We were talking about the store so let continue with that. In this case let think about action, mutation, or anything that will do an update. For example, let use the old setState that React uses in classes as an example.
// state
this.state = {
foo: "foo",
bar: "bar"
}
// mutation
this.setState({
foo: "foo"
})
The method setState needs to receive just a part of the whole state, but we can't use Pick or Omit, because we don't know which will be the key that will be omitted. So, for these cases, we need to send a "partial interface" that will be merged with the whole interface.
// state
interface State {
foo: string,
bar: string
}
// mutation
type SetState = (value: Partial<State>) => State;
But what is doing this Partial
behind the scene, well it's not so complicated. It's just adding optional to each first-level property.
// state
interface State {
foo: string,
bar: string
}
type PartialState = Partial<State>;
/*
This will result in a type like that:
interface PatialState {
foo?: string,
bar?: string
}
*/
You could find another case where you need to use it. Just remember that only put optional to first level properties, if you have nested object the child properties will not be impacted by this util.
readonly
If you like to work with immutable data maybe you'll love this keyword. Typescript allows you to determine which properties of your object could modify or not. Continue with the stores, if you will use the Flux architecture you don't want to allow that the state to be modified, you just want to recreate the state in each action.
So for these cases are helpful to put these properties as readonly because it will throw an error if anyone tries to modify it.
interface Stores {
readonly user: User,
readonly theme: Theme
// ... some others
}
Also, you could use the Readonly util
type ReadonlyStores = Readonly<Stores>
When you try to modify any value, you will see an error message.
const store: ReadonlyStores = {
user: new User(),
theme: new Theme(),
// ... some others
}
stores.user = new User()
// Error: Cannot assign to 'user' because it is a read-only property.
IMPORTANT
This check will throw an error in compile-time but not during runtime as const
does. It means that if you have a code that typescript is not tracking, it will easily modify your property in runtime. Just prevent skipping typescript rules from your files.
Smart use of infer typing
Typescript has a really powerful inference algorithm. This means that sometimes we don't need to be explicit with the type of a variable because it will directly be typed for you.
let a = "a" // Typescript infer that it will be a "string"
a = 3 // It'll throw an error
// Just need to specify the type if you are not passing a value to the variable
let a: string;
a = "a"
// In another way it will be typed as any
let a; // typescript typed as any (some config will prevent this automatic any type)
a = "a"
a = 3 // it will NOT throw an error
We could use this superpower for our benefit. Continue with our store, instead of creating the interface like that...
interface Stores {
user: User,
theme: Theme
// ... some others
}
const stores: Stores = {
user: new User(),
theme: new Theme()
}
... we could give the responsability to typescript to create it automatically.
const stores = {
user: new User(),
theme: new Theme()
}
type Stores = typeof stores;
The common typeof
keyword takes a new power in typescript. It will return the type that typescript infers of the declaration of the variable. So both codes are doing the same thing.
I love this feature because in these cases the type is completely dependent on the declaration. If you add a new field you just need to add it in the declaration and it will propagate to type immediately. Instead in the manual interface creation, you need to propagate this by your self which could come with some mistakes.
Conclusion
Typescript is fabulous, but as you could see with the difference between readonly and const, typescript just creates a layer for the developer to make the code more secure for all. But the JS code that is generated will not follow the rules. So it could modify the readonly property or have access to private attributes because it just a layer while you are coding.
Also if you are using classes to privatize some methods or attributes, it will be just "private" before compilation. If you really want to use a private value you could use a closure factory, also this could reduce just a little bit the bundle size of your compiled code because there now any necesity to compile anything like when you are using a class. If you are looking for a example of that let check this rewrite that Mark Erikson did in the Subscription of react-redux.
Remember this when you are working with typescript it will help you understand what is happening behind the scenes.
Thank you for reading this post. I hope this helps you in your daily work.
If you would like to learn more, I highly recommend the Typescript documentation for utils.
https://www.typescriptlang.org/docs/handbook/utility-types.html
Top comments (0)