В этой статье мы рассмотрим практические примеры использования TypeScript для повышения безопасности кода. TypeScript — это язык программирования, который расширяет JavaScript, действуя как его надмножество. Однако это всего лишь язык времени компиляции! Таким образом, ничего, связанное с типами (например, интерфейсы, объявление типа, присвоение типа и т. д.), не передается в JavaScript. Наоборот, это сверхмощный инструмент, который помогает нам, разработчикам, делать меньше ошибок и, таким образом, уменьшать количество ошибок во время выполнения.

С учетом сказанного давайте отправимся в путешествие, чтобы исследовать мощь TypeScript на практических примерах.

1 — Подпишите контракт и приступайте к реализации.

const add = (a:number, b:number ) => (a+b).toString();

В этом примере видно, что функция складывает два числа, и мы ожидаем получить обратно число.

Однако текущая реализация вместо этого возвращает строку. Чтобы убедиться, что функция возвращает правильный тип, вы можете указать свое намерение перед началом реализации, добавив аннотацию возвращаемого типа:

const add = (a:number,b:number):number => (a+b).toString()
// Type 'string' is not assignable to type 'number'

Использование аннотаций типов, как и указание возвращаемых типов, не только повышает читабельность кода, но также помогает идентифицировать и изолировать ошибки, близкие к их источнику. При реализации такой функции, как add, которая может использоваться в различных частях вашего кода, указание ожидаемого типа возвращаемого значения может предотвратить распространение ошибок по всему приложению. Добавляя аннотации типа возвращаемого значения к самому определению функции, вы сможете быстро выявлять и исправлять любые проблемы внутри функции, а не искать ошибки, разбросанные по кодовой базе.

2 – Любые или неизвестные

let a:any = 1;
a = '1'
a = null
a = undefined
a = {}
a = function(){}

// Let's use `a`
a() // we can call it
a.toUpperCase() // we an use string methods on it
a.non_existing_property.nested_non_existing_property // 🤔
a:unkown = 1;
a = '1'
a = null;
a = undefined
a = {}
a = function(){}

// let's use `a`
a() // error: 'a' is of type 'unknown'.
a.toUpperCase() // error: 'a' is of type 'unknown'.
a.non_existing_property // error 'a' is of type 'unknown'.

Вы заметили разницу? Если да, то поздравляю 🎉

Если нет, не беспокойтесь, это очень просто.

any и unkown могут быть любыми. (строка, число, объект, функция...)

Разница в том, что когда вы пытаетесь использовать any, он будет представлять себя как угодно.

Когда вы объявляете переменную или параметр функции с типом any, это, по сути, означает, что компилятор TypeScript не будет применять какие-либо правила проверки типов для этой переменной или параметра. Это позволяет вам использовать переменную, как если бы она имела любой тип, такой как строка, число или функция, не вызывая ошибок, связанных с типом.

С другой стороны, когда вы объявляете его как unkown, компилятор заставит вас выполнить проверку типа перед его использованием. error: 'a' is of type 'unkown'

Давайте посмотрим на практический пример:

const fetchUsers = async ():Promise<User[]> => {
  try {
    ... call server
  } catch(e){ // e:unkown
     
  }
}

В этом примере e наша ошибка имеет неизвестный тип. В современной версии TS тип ошибки в любом блоке catch по умолчанию равен unkown вместо any в предыдущих версиях.

Уже нашли причину?

// With any
const fetchUsers = async ():Promise<User[]> => {
  try {
    ... call server
  } catch(e:any){ 
     console.error(e.message) // the compiler is happy. 
                              // even if we don't have a property message
                              // on e
   }
}


// With unknown
const fetchUsers = async ():Promise<User[]> => {
  try {
    ... call server
  } catch(e:unknown){ 

     console.error(e.message) // error: 'e' is of type unkown

     // fix
     if(e != null 
        && typeof e ==='object' 
        && 'message' in e 
        && typeof e['message'] === 'string'){
         console.error(e.message) // compiler is happy.
       }
   }
}

3 — Используйте предикаты типов во всех ваших Type Guards.

Мы видели в предыдущем примере, что unknown нельзя использовать, пока мы не проверим тип перед этим.

const fetchUsers = async ():Promise<User[]> => {
  try {
    ... call server
  } catch(e:unknown){ 
     if(e != null 
        && typeof e ==='object' 
        && 'message' in e 
        && typeof e['message'] === 'string'){
         console.error(e.message) // compiler is happy.
       }
   }
}

Я знаю, что это немного грязно, верно? Нам также может понадобиться проверить ошибки в других блоках catch в нашем коде. Таким образом, мы не соблюдаем СУХОЙ (не повторяйтесь принцип)

Давайте сделаем это немного лучше:

const isErrorWithMessage(e:unkown):boolean {
  return e != null && typeof e ==='object' && 'message' in e 
        && typeof e['message'] === 'string';
}
const fetchUsers = async ():Promise<User[]> => {
  try {
    ... call server
  } catch(e:unknown){ 
     if(isErrorWithMessage(e)){
         console.error(e.message) // error: 'e' is of type 'unknown'.
       }
   }
}

ждать! почему? он работал нормально до рефакторинга условия в функцию? Что случилось?

Что ж, TypeScript, к сожалению, не запустит вашу функцию и не проверит, что внутри, чтобы убедиться, что вы можете безопасно получить доступ к «сообщению» из «неизвестного» типа e . В предыдущем примере логика присутствовала, поэтому TS достаточно умен, чтобы проанализировать ее и рассматривать e как объект с сообщением о свойствах типа string. но как только мы рефакторим логику в функцию, TS мало что может сделать.

type ErrorWithMessage = {
  message:string;
}
const isErrorWithMessage(e:unkown):e is ErrorWithMessage {
  return e != null && typeof e ==='object' && 'message' in e 
        && typeof e['message'] === 'string';
}
const fetchUsers = async ():Promise<User[]> => {
  try {
    ... call server
  } catch(e:unknown){ 
     if(isErrorWithMessage(e)){
         // e: ErrorWithMessage
         console.error(e.message) // error: 'e' is of type 'unknown'.
       }
   }
}

Это называется предикатом типа. e is ErrorWithMessage

Это говорит TS, что если функция возвращает true, вы должны обрабатывать e как ErrorWithMessage, поэтому, если мы наведем указатель мыши на e, мы увидим, что теперь это ErrorWithMessage вместо unknown.

Но будь осторожен!!! Вы должны убедиться, что знаете, что делаете внутри Type Guard.

type ErrorWithMessage = {
  message:string;
}
const isErrorWithMessage(e:unkown):e is ErrorWithMessage {
  return true;
}
let a:unkown;
if(isErrorWithMessage(a)){
  a // ErrorWithMessage
}

Если вы вернете true! Компилятор TS будет рассматривать a как ErrorWithMessage, даже если у него нет свойства сообщения! Мы несем ответственность за защиту безопасного типа.

4 — Используйте оператор Satisfies для устранения двусмысленности

type DbConfigKeys = 'port' | 'username' | 'pool'

export const DBConfiguration:Record<DbConfig, string | number>={
  'username':'root',
  'pool':3,
  'port':8080
}

Мы просто создаем объект DbConfiguration, который является Record.. Record — это просто объект, который принимает в качестве первого аргумента тип Key, а в качестве второго аргумента — тип Value.

В этом примере наш DbConfiguration является объектом с Key:DbConfigKeys
и значением может быть string или number .

import {DbConfiguration} from '...'
import db from 'db';

const initiDB = async ()=>{
  db.setPort(DBConfiguration.port) // setPort expects a number
 // Argument of type 'string' is not assignable to parameter of type 'number'.
 // Type 'string' is not assignable to type 'number'
  
}

Проблема здесь в том, что все values будут выводиться как string | numberдаже несмотря на то, что мы присвоили port число и usernameстроку.

Почему? потому что, когда мы впервые инициализировали DbConfiguration., мы уже присвоили ему тип Record<DbConfigKeys, string | number>, что означает, что key будет иметь тип DbConfigKeys, а значение будет иметь тип string | number. TS не будет заглядывать внутрь объекта после инициализации, чтобы определить точный тип каждого значения. Вместо этого он будет бесконечно рассматривать его как string | number

Мы можем решить проблему, удалив присвоение типа

export const DbConfiguration ={
  'username':'root',
  'pool':3,
  'port':8080
   // But now we can anything to our object 😢
   'randomProperty':1334343434
}

DbConfiguration.port // number
DbConfiguration.pool // number
DbConfiguration.username // string

Чтобы исправить это, мы можем использовать оператор удовлетворения, представленный в TS 4.9.

type DbConfigKeys = 'port' | 'username' | 'pool'
export const DbConfiguration={
  'username':'root',
  'pool':3,
  'port':8080
   'randomProperty':2333   // error: Object literal may only specify 
                          // known properties, and ''randomProperty'' 
                         // does not exist in type 
                        // 'Record<DbConfigKeys,string | number>

} satisfies :Record<DbConfigKeys, string | number>

DbConfiguration.port // number
DbConfiguration.pool // number
DbConfiguration.username // string

Мы успешно устранили двусмысленность и сохранили безопасность типов! Мы не можем добавлять случайные значения и свойства, имея при этом правильный и точный вывод типа для каждого значения! Удивительно 🎉

5 — Используйте универсальные шаблоны вместо любых

Обычно мы сталкиваемся с использованием any всякий раз, когда у нас есть функция, которая может принимать различные типы параметров. Например, когда мы хотим создать функцию, которая фильтрует массив объектов.

const filter = (array:any, key:any, value:any)=>{
  return array.filter((item)=>item[key] === value);
}

const arr = [{'name':'hedi', age:24},{'name':'ahmed',age:25}]

filter(arr,'nam','hedi'); // TS can't sport the Typo.
filter(null,'name','ahmed'); // will throw an error on runtime
filter(arr,'name',24); // name is string! but we're filtering by a number

Кажется знакомым?

Вместо этого давайте использовать дженерики! и все последует как по волшебству!

const filter = <T extends object, Key extends keyof T>(arr:T[],
                                                        key:Key,
                                                        value:T[Key]){
 return arr.filter((item:T)=> item[key] === value);
}

// Here we're trying to make it generic
// 1 - T extends object is a constraint so we only pass objects! no primitve types
// 2 - Key extends keyof T: Key should be one of the keys of T
// 3 - arr:T[] is an array of T
// 4 - key is of type Key which is one of the keys of T
// 5 - value:T[Key] this is called indexed access type to get the value type of
// the key we're passing

Вот как typescript выведет функцию фильтра, когда мы вызовем ее с этим массивом ниже.

const arr = [{'name':'hedi', age:24},{'name':'ahmed',age:25}]

filter(arr,'name','hedi');

// T is {'name':string, age:number}
// Key is keyof T = 'name' | 'age'
// T[key] is T[Key] = T['name' | 'age'] = string | number

Попробуем еще раз вызвать функцию с теми же параметрами, что и раньше.

const arr = [{'name':'hedi', age:24},{'name':'ahmed',age:25}]

filter(arr,'nam','hedi');
// error:Argument of type '"nam"' is not assignable to parameter of type '"name" | "age"
filter(null,'name','ahmed'); 
// error:Argument of type 'null' is not assignable to parameter of type 'object[]'
filter(arr,'name',24); 
// error:Argument of type 'number' is not assignable to parameter of type 'string'

И вот оно! Обобщения невероятно мощны, поэтому рассмотрите возможность их использования вместо типа any, которого следует избегать, когда это возможно, для обеспечения безопасности типов и качества кода.

Заключение

В этой статье мы рассмотрели практические примеры использования TypeScript для повышения безопасности и удобочитаемости кода. TypeScript, надмножество JavaScript, добавляет ценные функции, такие как аннотации типов и обобщения, для улучшения обнаружения ошибок и удобства сопровождения кода.

В наших примерах мы продемонстрировали силу TypeScript в обеспечении безопасности типов и избегании распространенных ошибок. Мы показали, как задавать возвращаемые типы, используя тип unknown вместо any, реализовывая предикаты типов в охранниках типов, используя оператор satisfies и используя дженерики, все это может способствовать созданию более надежного и удобного в сопровождении кода.

В заключение можно сказать, что TypeScript — это мощный инструмент для повышения безопасности и удобочитаемости кода, но он требует осторожного использования и понимания его функций, чтобы максимизировать его преимущества. Используя возможности TypeScript и по возможности избегая использования типа any, вы можете улучшить качество кода и удобство сопровождения, что приведет к созданию более надежных и надежных веб-приложений.