Few realisations:
type Subscription = {
name: string;
price: number;
recurrence: "annual" | "monthly" | "weekly";
};
type Price = Subscription["age"];
// number
type Recurrence = Subscription["recurrence"];
// 'annual' | 'monthly' | 'weekly'
type NameOrPrice = Subscription["name" | "price"];
// string | number
keyof
keywordkeyof
allows to generate a type from an object’s keys (properties).
type Subscription = {
name: string;
price: number;
recurrence: "annual" | "monthly" | "weekly";
};
type SubscriptionKeys = keyof Subscription;
// 'name' | 'price' | 'recurrence'
You can easily create a helper type ValueOf
allowing you to get all the possible types of object’s properties:
type ValueOf<Obj> = Obj[keyof Obj];
type SubscriptionValues = ValueOf<Subscription>;
// string | number | 'annual' | 'monthly' | 'weekly'
JavaScript has typeof
operator that gives you the type of an object. You can combine it with keyof
to retrieve the keys of the object.
keyof typeof
is a common pattern used to create types representing the keys of an object. It’s useful when you don’t know the type of the object. For example when you have an instance of the object created in place const obj = { name: 'Base plan'}
. Consider example below:
const subscription = {
name: "Base plan",
price: 100,
recurrence: "annual",
};
type SubscriptionKeys = keyof typeof subscription;
// 'name' | 'price' | 'recurrence'
If you want to check the types of the nested properties of the passed objects you can use the following notation:
export const getIn = <
TObj,
TKey extends keyof TObj,
TNestedKey extends keyof TObj[TKey],
>(
obj: TObj,
key: TKey,
nestedKey: TNestedKey,
) => {
return obj[key][nestedKey];
};
const user = {
address: {
street: "420 9th Ave",
country: "US",
city: "New York",
},
};
getIn(user, "address", "street");
typeof
keywordFor large, complex objects with deeper nesting levels, manually defining their types can be tedious.
const user = {
address: {
street: "420 9th Ave",
country: "US",
city: "New York",
},
};
type User = typeof user;
type Address = User["address"];
To get more precise types you can use as const
construct.
const address = {
street: "420 9th Ave",
country: "US",
city: "New York",
};
typeof user;
/*
{
street: string,
country: string,
city: string,
}
*/
const addressStatic = {
street: "420 9th Ave",
country: "US",
city: "New York",
} as const;
typeof addressStatic;
/*
{
readonly street: string,
readonly country: string,
readonly city: string,
}
*/
Sometimes you want to highlight that property can be missing on object and you’re fine with that. For that you’re using optional symbol ?
:
type Subscription = {
name: string;
price: number;
recurrence: "annual" | "monthly" | "weekly";
endDate?: Date; // optional property
};
It’s possible to combine multiple existing types into a new one by using intersection &
operator. The new type will have all of the properties of the used types.
type PrimaryAttrs = {
name: string;
price: number;
};
type RecurranceAttrs = {
recurrence: "annual" | "monthly" | "weekly";
endDate?: Date;
};
type Subscriptioon = PrimaryAttrs & RecurranceAttrs;
Intersections are applied recursively on all the properties. If there is a property present in both of the types it will also be intersected.
Typescript provides utility type Record
which you can define as following:
type Record<K, V> = {
[key in K]: V;
};
It leverages JavaScript standard in
operator. It returns true
if the specified property key
is in the specified object K
.
Record
can be used when you need to construct an object with entries of the same type:
type FeatureFlags = Record<string, boolean>;
Typically used when there is a need to construct a new type by picking a subset of properties:
type Subscription = {
name: string;
price: number;
recurrence: "annual" | "monthly" | "weekly";
};
type Recurrence = Pick<Subscription, "recurrence">;
// instead of alternative
type RecurrenceAlt = {
recurrence: "annual" | "monthly" | "weekly";
};
type PrimaryAttrs = Pick<Subscription, "name" | "price">;
// instead of alternative
type PrimaryAttrsAlt = {
name: string;
price: number;
};
Kind of opposite to Pick
in the way it allows to construct a new type by omitting specified keys:
type Subscription = {
name: string;
price: number;
recurrence: "annual" | "monthly" | "weekly";
};
type PrimaryAttrs = Omit<Subscription, "recurrence">;
// instead of alternative
type PrimaryAttrsAlt = {
name: string;
price: number;
};
type Recurrence = Omit<Props, "name" | "price">;
// instead of alternative
type RecurrenceAlt = {
recurrence: "annual" | "monthly" | "weekly";
};
By using Pick
and Omit
we can create other util functions. Here is how you would emulate the behaviour of JavaScript assign
that allows you to do { ...a, ...b }
but on types:
type Assign<A, B> = Omit<A, keyof B> & B;
// it overrides properties of `A` with properties of `B`
const assign = <A, B>(obj1: A, obj2: B): Assign<A, B> => ({
...obj1,
...obj2,
});
It works because intersections are applied recursively.
infer
is most often used in conditional types to find a type from another type. It allows you to put inferred type into variable and use it later. It can be used with conditional branches.
Here is an example of inferring return type from promise.
type GetPromiseReturnType<T> = T extends Promise<infer R> ? R : never;
type PromiseType = GetPromiseReturnType<Promise<number>>;
// type PromiseType = number
satisfies
Let’s consider the following annotation:
const subscriptions: Record<string, number> = {};
In this case Record<string, number>
operates on the subscriptions
variable.
We can try to put the limitations on the value variable holds:
const subscriptions = {
free: 0,
basic: 75,
premium: 120.5,
} satisfies Record<string, number>;
In such context with satisfies
you can make sure that condition is met when applied to the value. With such notation you wouldn’t be able to assign new properties to subscriptions
as it’s narrowing down the type. Most likely that’s not what you want.
unknown
vs any
unknown
type is safer than any
. Typescript makes you check the type before you can use variable of type unknown
.
any
you can skip type checking. Variables of this type can be set to anything else and code wouldn’t fail to compile.