visit
Imagine you want to count items, which have the field meta
that contains the field countable
set to true
, in an array. If an item doesn't have meta.countable
, we don't count it.
function getAmount (arr: any[]) {
return arr.filter(item => item.meta.countable === true).length;
}
Typescript array type with anything in there
Why are you using any
? It's not OK! Don't use any
in such cases. Read more about that later on in this article. We see the any
keyword near the arr
argument, that's new to us! I guess you already know what it is. This way we tell TS that we’re expecting a parameter called arr
of any[]
type.
It literally means any Javascript type can be passed into the array. I.e. arr
is an array and every item of it is of type any
. Using the any[]
type means you’ll get type checking. If you pass in an argument that’s not an array, you’ll end up with the errors below:
// Argument of type 'string' is not assignable to parameter of type 'any[]'
getAmount('string');
// Argument of type 'number' is not assignable to parameter of type 'any[]'
getAmount(29);
The compiler ensures you should pass exactly what you've pointed out as an arr
argument for the getAmount
function. What if you need to pass several types, for example, an array and a string? And if arr
is a string, then return 0. A weird case, but imagine you work on a legacy system that uses this function in many places.
function getAmount (arr: any[] | string) {
if (typeof arr === 'string') {
return 0;
}
return arr.filter(item => item.meta.countable === true).length;
}
getAmount('55'); // now it's possible to pass a string
getAmount([{ meta: {countable: true} }]);
|
means "or". Thus, arr
can be an array containing values of any type (any[]
) or a string. Refer to for more everyday types in Typescript.
The compiler is smart enough to even infer a return type of getAmount
.
// function getAmount(arr: any[] | string): number
function getAmount (arr: any[] | string) {
// because we always return a number
// 0 or arr.length(filtered
}
Type inferring for a function that always returns a number
function getAmount(arr: any[] | string): number {
// ...
}
function myFunction(arg: any): boolean {/* function body */}
function getAmount(arr: any[]): number {
// ...
}
getAmount([5, "string", {}, () => {}]); // no error
That's not what we expect. TS works well in this case, we specified any[]
, so what’s the problem?
Don't use any
if there's no real need for it. It's easier to pass any
than describing an advanced type, but that's what Typescript is for. Future-proof your application
We may want to replace any[]
with object[]
and it would work if we pass objects there right? Correct, but null
and functions are also objects. They’re also not what we expect either.
Don't use object[]
, try to narrow the types.
interface Item {
meta?: {
countable?: boolean;
}
}
function getAmount (arr: Item[]) {
return arr.filter(item => item.meta?.countable === true).length;
}
getAmount([
{}, {meta: {countable: true}}
]); // 1
Now it works as expected. We specified a separate interface
for a possible array element. Interfaces and types allow you to create your own types using basic Typescript types. Some examples:
// is also called "type alias"
type Hash = string;
// interface are "object" types and allow us
// to specify an object immediately
interface Person {
name: string;
isOkay: boolean;
};
// it's the same as using a type alias
type Person = {
name: string;
isOkay: boolean;
};
type Person = {
name: string;
}
type Ticket = {
from: string;
to: string;
person: Person;
}
function bookTicket (from: string, to: string, person: Person): Ticket {
// some procesing
return {
from,
to,
person,
};
}
bookTicket('Paris', 'Mars', {name: 'Joey'});
The code seems okay. However, we can book a ticket to Mars using the function, but we don't fly to Mars yet. What can we change in our code to reflect this? We could add validation for from
and to
fields inside the function, but we can simply do this with TypeScript instead. For example, we could list possible locations we're flying to and from.
type AvailableLocation = 'Paris' | 'Moon' | 'London';
type Person = {
name: string;
}
type Ticket = {
from: AvailableLocation;
to: AvailableLocation;
person: Person;
}
function bookTicket (from: AvailableLocation, to: AvailableLocation, person: Person): Ticket {
// some procesing
return {
from,
to,
person,
};
}
// Error: Argument of type '"Mars"' is not assignable to parameter of type 'AvailableLocation'
bookTicket('Paris', 'Mars', {name: 'Joey'});
We narrowed the possible options for locations. Thus, eliminated cases when we can write code that calls the function with invalid locations like "Mars" or "Andromeda Galaxy". We listed multiple allowed options via "or" operator - Paris | Moon
. We might be using enums for this purpose too:
enum Locations {
Paris,
Moon,
London,
}
type Ticket {
from: Locations;
to: Locations;
person: Person;
}
bookTicket(Locations.Paris, Locations.Moon, {name: 'Joey'});
As you might notice, somewhere I used interface
for an object type and then declared another one via type
. You may use whichever you prefer for such cases or choose based on your project code guidelines. For more information about the difference, .
Record
to Type ObjectsSometimes you have generic objects, where a key is always string
(and it's always a string, if you want to use other values, use Map
instead) and a value is always string
too. In this case, you may define its type as follows:
type SomeObject = {
[key: string]: string;
}
const o: SomeObject = {key: 'string value'}
There's another way to do the same using Record<keyType, valueType>
:
type SomeObject = Record<string, string>;
// it means an object with string values, e.g. {who: "me"}
It's something new here: , computed types to re-use the existing ones. Let's re-create the Record
type:
type Record<Key, Value> = {
[key: Key]: Value;
}
const obj: Record<string, number> = {level: 40, count: 10};
type StateItem = {
isLoading: boolean;
response: Record<string, unknown> | null;
};
type State = Record<string, StateItem>;
const state: State = {
getInvoices: {
isLoading: false,
response: null,
},
};
Do you see the inconveniences here? We might narrow a type for state
keys: it's a string, but we want to be sure we put valid API request names there. The second thing is the unknown
I put for the response
(an object with unknown
values), yet it's still better than any
, because you should determine its type before any processing.
type APIRequest = 'getInvoices' | 'getUsers' | 'getActions';
type BaseResponse = {isOk: boolean};
type GetInvoicesResponse = BaseResponse & {data: string[]};
type GetUsersResponse = BaseResponse & {data: Record<string, string>[]};
type GetActionsResponse = BaseResponse & {data: string[]};
type StateItem = {
isLoading: boolean;
response?: GetInvoicesResponse | GetUsersResponse | GetActionsResponse;
};
type State = Record<APIRequest, StateItem>;
// Type is missing the following properties from type 'State': getUsers, getActions
const state: State = {
getInvoices: {
isLoading: false,
response: {isOk: false, data: ['item']},
},
};
APIRequest
type is a list of possible requests names. Narrowing types are for the better. See the error comment near the state
const? Typescript requires you to specify all the requests.
BaseResponse
represents a default and basic response, we always know that we receive {isOk: true | false}
. Thus, we may prevent code duplication and re-use the type.
While it's better than it was before, we could do even better. The problem with these types is that response
is too generic: we may have GetInvoicesResponse | GetUsersResponse | GetActionsResponse
. If there are more requests, there is more ambiguity. Let's employ generics to reduce duplicate code.
type BaseResponse = {isOk: boolean;};
type GetInvoicesResponse = BaseResponse & {data: string[]};
type GetUsersResponse = BaseResponse & {data: Record<string, string>[]};
type GetActionsResponse = BaseResponse & {data: string[]};
type StateItem<Response> = {
isLoading: boolean;
response?: Response;
};
type State = {
getInvoices: StateItem<GetInvoicesResponse>;
getUsers: StateItem<GetUsersResponse>;
getActions: StateItem<GetActionsResponse>;
};
It's more readable and safe to specify every request separately, thus there's no need to check state.getInvoices.response
on every response type possible.
Don't use any
type. Prefer unknown
. You should check the type before doing any further operations with it.
type Obj = Record<string, unknown>;
const o: Obj = {a: 's'};
o.a.toString(); // Object is of type 'unknown'
2. Prefer Record<string, T>
over object
, which can be null
, any kind of object, a function. T
refers to a generic type.
3. Narrow types where possible. If it's a few strings you use often, probably they can be combined in one type(see the example about API requests state).
type GoogleEmail = `${string}@gmail.com`; // yet it's still a string
const email1: GoogleEmail = '[email protected]';
// Type '"[email protected]"' is not assignable to type '`${string}@gmail.com`'
const email2: GoogleEmail = '[email protected]';
type Response<T> = {
isOk: boolean;
statusCode: number;
data: T;
}
async function callAPI<T> (route: string, method: string, body: unknown): Response<T> {
// it's a pseudo-fetch, the real API differs
const response = await fetch(route, method, body);
// some manipulations with data
return response;
}
So, the syntax is function <name>:<type> (args) {}
. You may use T
(or other names for a generic, or, a few of them) inside a function too.
type AccessToken = string;
type IdToken = string;
function callProviderEndpoint (token: AccessToken) {}
function decodeUserInfo (token: IdToken) {}
So, the syntax is function <name>:<type> (args) {}
. You may use T
(or other names for a generic, or, a few of them) inside a function too.
There are cases when you need to cast(transform for the compiler) a type to another one. For example, when a library method returns an object and you know it's not useful, you need a more narrow type. You may write const result = libResult
as Record<string, number>
as it allows you to transform a type into a desired one(if it's possible). The easiest cast is for any types: the compiler doesn't know anything about a value, so it trusts you. There are cases when you'd want to cast something into any
for compatibility, but often it's laziness to to do so.
const response = <MyCorrectType>libResponse;
// the same as
const result = libResponse as MyCorrectType;
First Published