Как стать автором
Обновить
215.11
НЛМК ИТ
Группа НЛМК

Почему мы используем Typescript в своих проектах и каковы его расширенные возможности?

Уровень сложностиСредний
Время на прочтение16 мин
Количество просмотров1.1K
TypeScript и его части-пазлы
TypeScript и его части-пазлы

Почему нам недостаточно просто «писать работающий код»? В НЛМК проекты постоянно масштабируются и улучшаются, и чаще всего над ними работают большие команды разработчиков с разным опытом, со своими подходами и видением. В фронтенд-разработке можно выбрать обычный JavaScript и радоваться жизни, но, к сожалению, его использование принесет много проблем с поддержкой в будущем ввиду отсутствия типизации и других его “странностей”. 

Да, можно сказать, что код должен быть самокомментируемым и понятным для всех, но это ситуация в вакууме, никак не относящаяся к реальности. Поэтому использование языка TypeScript – это хорошая инвестиция в будущее: он не просто добавляет строгую типизацию в JavaScript – он позволяет вам проектировать приложения, строить понятную архитектуру, экономить нервы разработчиков и учить джунов писать чистый и безопасный код без «костылей».

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

Основные возможности, минусы и плюсы Typescript

Typescript – типизированный вариант языка Javascript. Код, написанный на Typescript (далее - TS), компилируется в JS, благодаря чему можно писать код под любые браузеры. Все фишки, особенности и, возможно, недостатки JS – уже идут в комплекте с TS. Тут все просто.

Важное нововведение - статическая типизация. То есть теперь никаких “0 == ‘0’” и так далее. Так же добавлены элементы ООП – типы, интерфейсы, объекты, классы, наследование, а также утилиты, дженерики и так далее. Именно с этими возможностями будем подробно работать в этой статье. 

Из минусов можно выделить более высокий порог входа, нежели в JS, так как помимо основ языка нужно разбираться в ООП, время компиляции в JS (не самый существенный минус) и поддержка сторонних библиотек – иногда они попросту не написаны, недоступны или не самого хорошего качества. Но в данной статье эти минусы нам не помешают.

Основы - типы, интерфейсы, перечисления и прочие “звери”

Рассмотрим основные понятия TS, с которыми будем работать дальше:

  • Перечисления (enum) – структура-коллекция связанных значений. Используются для объединения нескольких значений, которые объединены по одному признаку: по цвету, по типу и так далее – такое объединение может быть любым. Например, создадим перечисление по возможным HTML-элементам в форме:

enum Component { 
 BUTTON,
 INPUT,
 CHECKBOX,
 SELECT
}

Подробнее про перечисления можно узнать в статье коллеги: https://habr.com/ru/companies/nlmk/articles/770974/

  • Типы (type) – структура для создания псевдонимов, которые обозначают определение какое-либо другой структуры. Это значит, что тип может быть как псевдонимом для базового типа в TS (number или string, например), так и для комплексного объекта. Например, создадим тип для компонента кнопки:

type Button = { 
 id: number,
 label: string,
 color: string,
}

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

  • Интерфейсы (interface) – структура для создания “синтаксического контракта” объекта. Например, пример типа Button также можно реализовать в виде интерфейса:

interface Button { 
 id: number;
 label: string;
 color: string;
}

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

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

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

Далее рассмотрим темы от простого к сложному, которые превратят вас в гуру языка Typescript. Приступим!

Дженерики, операторы keyof и typeof – “копаемся” в основах

  • Оператор keyof позволяет извлечь тип объекта и делает из него строковый или числовой список его ключей (ключевое слово key на это намекает). Например, возьмем наш пример с type Button выше:

type Button = {
 id: number,
 label: string,
 color: string,
}

type TButton = keyof Button // type TButton = ‘id’ | ‘label’ | ‘color’
const buttonKey: TButton = 'label';

Мы ограничили возможные значения переменной buttonKey до 3х - ключей типа Button. Присвоить ей что-то другой не получится.

  • Оператор typeof извлекает тип переменной или какого-либо свойства объекта. Рассмотрим пример:

const button = 'buttonValue'
type TString = typeof button

const newButton: TString = 'newButtonValue'

Данный код не совсем корректен из-за такого понятия как литеральный тип: он возникает, когда TS пытается извлечь тип из переменной с константным значением, и как результат – TString содержит не просто базовый тип string, как ожидалось, а строковый литеральный тип buttonValue. Исправим:

let button = 'buttonValue'
type TString = typeof button

const newButton: TString = 'newButtonValue' // Ваше новое значение для кнопки прекрасно!

Теперь TString будет содержать общий тип string, не литеральный как в случае с переменной, объявленной через const. В случае с const можно “обойти” создание литерального типа через явное указание типа переменной:

const button: string = 'buttonValue'
type TString = typeof button

const newButton: TString = 'newButtonValue'
  • Дженерики (Generic Types) – это обобщенные типы. Иногда они необходимы, когда требуется описать тип или интерфейс (или функцию) с поддержкой другого переданного типа. Это своеобразный шаблон для создаваемых типов или многоразовая функция с параметром-типом. Рассмотрим пример:

function print(value: string) { 
 console.log(value)
}

print('hello')
print(2025)

Первый вызов функции пройдет без проблем. Во втором появится ошибка, так как список передаваемых типов данных был ограничен до string. Да, можно воспользоваться any, но это плохой вариант, так как в дальнейшем информация о типе передаваемой переменной будет утерян. Вдруг мы вместо вывода значения переменной в консоль захотим с ней поработать и вернуть, но только с тем же типом данных:

function printAndReturn(value: any): any {
 console.log(value)
 return String(value)
}

printAndReturn('hello')
printAndReturn(2025)

Разработчик выполнил приведение к строке переменной value, и теперь принимаемый и возвращаемые типы не совпадают. Воспользуемся дженериком:

function printAndReturn<T>(value: T): T {
 console.log(value)
 return String(value) // Type 'string' is not assignable to type 'T'
}

printAndReturn('hello')
printAndReturn(2025)

Теперь такой фокус с “внезапной” сменой типа не пройдет: нужно вернуть переменную с тем же типом, что и был для передаваемого параметра value. Как видно из примера, нужно указать функции название дженерика, после чего его можно использовать при описании передаваемых и возвращаемых параметров.

Применим дженерики для интерфейса:

interface ValueWithGeneric<T> {
   value: T;
   getValue: () => T;
}

const stringItem: ValueWithGeneric<string> = {
   value: "Hello",
   getValue: function() { return this.value; } // Изменили на обычную функцию
};

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

Conditional и mapped типы – условия и перебор в типах? 

В обычных языках программирования были и есть условные конструкции – в зависимости от выполнения условия выполнение кода идет по определенному сценарию. Определим несколько простых интерфейсов кнопки и селекта:

interface IComponent {
 id: number
 label: string
}

interface IButton extends IComponent {
 color: string;
}

interface ISelect extends IComponent {
 options: Array<string>
}

Мы применили операцию наследования extends: ее используют, когда требуется расширить исходный интерфейс новыми полями. Например, в IButton появилось новое строковое поле color (куда же без покраски кнопочек). Аналогично интерфейс селекта ISelect наследует содержимое IComponent и расширяется новым полем options. Применим условные типы для них, чтобы определить тип компонента и положить их в отдельные типы:

type ComponentType<T> = T extends IButton ? "Button" : T extends ISelect ? "Select" : "Unknown Component";

type ButtonType = ComponentType<IButton>;  // "Button"
type SelectType = ComponentType<ISelect>;    // "Select"
type ComponentTypeUnknown = ComponentType<IComponent>; // "Unknown Component"

Здесь можно рассматривать extends как условия в любом другом языке программирования: 

if (условие) {
 тело, когда условие выполняется
} else {
 тело, когда условие не выполняется
}

// или

условие ? тело, когда условие выполняется : тело, когда условие не выполняется

Тип ComponentType проверяет – является ли переданный тип T дочерним от типа TButton? Если да, возвращается строка “Button”, если нет, то выполняется следующая аналогичная проверка для типа ISelect. Если же тип T так же не является дочерним от TSelect, то возвращается строка “Unknown Component” и проверка завершается. Итогом будет строка, занесенная в отдельный тип. Чтобы ее получить – нужно применить дженерик, который был рассмотрен выше. 

Далее мы разберем более сложный пример, но предварительно рассмотрим mapped types.

Иногда требуется перебрать параметры исходного типа или интерфейса и, по определенному правилу собрать новый тип/интерфейс на основе исходного. Здесь и пригодится использование mapped types (по-русски это называют сопоставлением типов). Для этого используется ключевое слово in в связке с ключевым словом keyof:

type NullProperty<T> = {
 [Property in keyof T]: T[Property] | null
}

type TComponentNullProperty = NullProperty<IComponent>

// type TComponentNullProperty = {
//     id: number | null;
//     label: string | null;
// }

Ключевое слово keyof возвращает объединение ключей (в нашем случае “id” | “label”), которое можно перебрать ключевым словом in. Получить тип конкретного поля можно, если подставить ключ в переданный тип (T[Property]). И все! Дальше с полученной информацией можно делать что угодно, в нашем случае – сделать все параметры переданного типа nullable. 

Как можно было понять, keyof возвращает union. Это можно использовать для получения типов полей, причем так же в виде union:

type TComponentKeys = keyof IComponent
// TComponentKeys = "id" | "label"

type TComponentKeyTypes = IComponent[TComponentKeys]
// type TComponentKeyTypes = string | number

Также для mapped types используются специальные модификаторы (например, readonly и ?), которые можно удалять или добавлять с помощью операторов “-” или “+”:

interface ICheckbox extends IComponent {
 checked: boolean
 required?: boolean
}

type CustomRequired<T> = {
 [K in keyof T] -?: T[K]
}

type TCheckboxRequired = CustomRequired<ICheckbox>

// type ICheckboxRequired = {
//   checked: boolean;
//   required: boolean;
//   id: number;
//   label: string;
// }

Для примера мы добавили опциональное поле required, которое нужно сделать обязательным. С помощью оператора “-” убираем “?”, если необходимо, после чего получаем новый тип TCheckboxRequired, в котором все необязательные поля стали обязательными.

Теперь мы готовы объединить полученные знания для более сложного примера:

type MandatoryProperties<T> = {
   [K in keyof T]-?: undefined extends T[K] ? never : K
}[keyof T];

type CheckboxMandatoryProps = MandatoryProperties<ICheckbox>;
// type CheckboxMandatoryProps = "checked" | "id" | "label"
  • ключевое слово never используется, чтобы отфильтровать ненужные поля;

  • undefined extends T[K] нужно, чтобы определить, содержится ли тип undefined среди доступных типов данного поля. При этом с помощью extends возвращается never, если содержится, и сам ключ (имя поля, не тип) в противном случае. Этот трюк нужен для следующего шага; 

  • [keyof T] применяется к результату, чтобы получить union типов полей, но так как там теперь не типы, а имена полей, то вернется именно union имен обязательных полей (never в результат не попадет). 

И таким образом можно писать самые разные дженерики, которые можно использовать и переиспользовать в своих проектах.

Ключевое слово infer – задачка со звездочкой

Для чего нужен infer? Представьте, что вам нужно “вывести” или извлечь тип из другого типа. Для этого нужно использовать infer в условных типах, что позволит временно “присвоить” тип переменной внутри проверки типов, который затем можно использовать. 

Рассмотрим самый простой пример с дженериком CustomReturnType:

type CustomReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

type StringType = ReturnType<() => string | string[]>
// type StringType = string | string[]

type AnyType = ReturnType<() => any>
// type AnyType = any
  • проверяем, является ли T функцией, если да, то будет возвращен тип, если нет, то тип any;

  • infer стоит перед R – оператор извлекает тип R и заносит его в ту же “переменную” (извлеченный тип теперь “называется” R).

В примере выше infer как бы “добывает” тип возвращаемого значения функции и переиспользует, чтобы вернуть его как результат типа.

Рассмотрим другой интересный пример:

type ReverseType<T> = T extends ${infer R}${infer K} ? ${ReverseType<K>}${R} : T

type ReversePasswordType = ReverseType<'qwerty123'> // '321ytrewq'
type ReverseHelloType = ReverseType<'привет'> // 'тевирп'

Здесь уже сложнее, так как тип применяется рекурсивно (“вызов самого себя”). Разберем этот пример поэтапно, разбивая на части:

  • разбиение c помощью шаблонных строк заносит в первую часть первый символ, во вторую – все остальные (R и K соответственно):

type ReverseTypeTrainee<T> = T extends ${infer R}${infer K} ? ${R}.${K} : T

type ReverseTypeTraineeExample = ReverseTypeTrainee<'qwerty123'> // 'q.werty123'
  • поменяем полученные литеральные типы местами (и уберем точку, которая была добавлена для наглядности):

type ReverseTypeTrainee<T> = T extends ${infer R}${infer K} ? ${K}${R} : T

type ReverseTypeTraineeExample = ReverseTypeTrainee<'qwerty123'> // 'werty123q'
  • данную операцию нужно “провернуть” для оставшейся части переданной строки, сделать это можно с помощью рекурсивного вызова. Поэтому заменяем ${R}${K} на  ${ReverseType<R>}${K} и получаем нужный результат.

Разберем заключительный пример с использованием infer и двинемся дальше – напишем дженерик для получения последнего элемента передаваемого массива. Воспользуемся spread оператором и определим последний элемент с помощью infer:

type Last<T extends any[]> = T extends [...args: any, infer L] ? L : never

type LastQWERTY = Last<['q', 'w', 'e', 'r', 't', 'y']>
// type LastQWERTY = "y"

Type inference – он умный, правда, но помощь не помешает

Язык TS самостоятельно определяет тип значения переменной (“лучший обобщенный тип”), например:

const password = ['q', 'w', 'e', 'r', 't', 'y', 1, 2, 3]
// const password: (string | number)[]

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

// см. интерфейсы IButton и ISelect выше
const button: IButton = {
 id: 1,
 label: 'Кнопка',
 color: 'blue'
}

const select: ISelect = {
 id: 2,
 label: 'Селект',
 options: ['Первый', 'Второй', 'Расчет окончен']
}

const components = [button, select];
// const components: (IButton | ISelect)[]

Среди элементов массива components нет ни одного элемента интерфейса IComponent, поэтому итоговый тип списка – массив элементов типов IButton и ISelect. Добавим безымянный элемент интерфейса IComponent:

const unknownComponent: IComponent = {
 id: 777,
 label: 'Неизвестный компонент'
}

const components = [button, select, unknownComponent];
// const components: IComponent[]

Теперь приведенный тип элементов – IComponent, который выведен (inferred) динамически из unknownComponent. Но чаще всего такой элемент добавлять не нужно, поэтому можно самостоятельно присвоить тип массиву компонентов:

const components: IComponent[] = [button, select];
// const components: IComponent[]
// кто бы мог подумать...

Проще говоря, Typescript автоматически присваивает типы к своим переменным на основе типа присвоенного переменной значения. Разработчик сам может явно типизировать переменную, тогда TS будет проверять присвоенное значение на уже его основе, без вывода типа (type inference). 

Данная возможность TS применяется и к переменным, и к функциям (в том числе и к передаваемым параметрам, и к возвращаемому результату), и к циклам и так далее. Например:

// обратите внимание: используется type inference
const components = [button, select];

for (let component of components) {
 console.log(component.id)
}

Type guards – БУ! Испугался? Не бойся, я тебя защищу!

Язык TypeScript, как мы выяснили, может определять типы на этапе компиляции, но иногда бывают случаи, когда нужно указать явно тип переменной. Рассмотрим случаи, когда можно использовать получаемую информацию о типах для выполнения пользовательских действий.

  • Рассмотрим использование знакомого нам typeof, обычно его используют, когда требуется проверка на принадлежность к примитивному типу (number, string, boolean, object и т.д.). Такие проверки также называют Type Narrowing. Допустим, значение border-а кнопки может быть как числом, так строкой. Во втором случае ничего изменять не нужно, но в случае числа переданное значение нужно дополнительно обработать:

interface IButton extends IComponent { 
 border: string
}

const button: IButton = {
 id: 1,
 label: 'Кнопка',
 border: '1px'
}

const setBorder = (border: string | number) => {
 // если строка, то оставляем как есть
 if (typeof border === 'string') {
   button.border = border
   return
 }

 // если число, то приведем значение к нужному формату css
 // данную проверку можно пропустить, тк TS будет знать, что это number, не string
 if (typeof border === 'number') {
   button.border = ${border}px
 }
}

setBorder('2px')
console.log(button.border) // ‘2px’

setBorder(5)
console.log(button.border) // ‘5px’
  • Введем новый оператор – instanceof, который проверяет на принадлежность значения к типу, который определяется классом. Рассмотрим пример и введем классы для компонентов, кнопок и селектов:

// вернем поле цвета для примера
interface IButton extends IComponent {
 color: string
}

class Component implements IComponent {
 constructor(public id: number, public label: string) {}
}

class Button extends Component implements IButton {
 constructor(id: number, label: string, public color: string) {
   super(id, label);
 }
}

class Select extends Component implements ISelect {
 constructor(id: number, label: string, public options: Array<string>) {
   super(id, label);
 }
}

Ключевое слово extends используется для наследования классов (почти так же, как и в случае с наследованием интерфейсов), implements – для реализации классом интерфейса, поля интерфейса станут public полями класса. Ключевое слово constructor используется для создания нового объекта и передачи ему значений полей, а super – чтобы обратиться к конструктору класса родителя. Но это если вкратце, желательно ознакомиться с классовым подходом в TypeScript дополнительно.

function describeComponent(component: Component) {
 // если это объект класса Button, то выводим цвет
 if (component instanceof Button) {
   console.log(Это кнопка, цвет: ${component.color});
 // если это объект класса Select, то выводим список его опций
 } else if (component instanceof Select) {
   console.log(Это селект с опциями: ${component.options.join(", ")});
 }
}

const components: Component[] = [
 new Button(1, "Кнопка 1", "Красный"),
 new Select(2, "Селект 1", ["Первый", "Второй", "Расчет окончен!"]),
];

components.forEach(describeComponent);
// "Это кнопка, цвет: Красный"
// "Это селект с опциями: Первый, Второй, Расчет окончен!"
  • Также можно составлять собственные предикаты (логические выражения, равные true/false) с использованием оператора is, например, для составления предикатов о принадлежности идентификатора указанному типу. Например, требуется написать функцию, проверяющую является ли параметр функции сущностью класса Button:

class Component implements IComponent {
 constructor(public id: number, public label: string) {}
}

class Button extends Component implements IButton {
 constructor(id: number, label: string, public color: string) {
   super(id, label);
 }
}

function isButton(component: Component): component is Button {
 return component instanceof Button;
}

console.log(isButton(new Button(1, "Кнопка 1", "Красный")))
// true

Но если в ходе проверок переданный параметр не принадлежит ни одному из классов, то его тип станет never:

function resetComponent(component: Button | Select) {
  if (component instanceof Button) {
    // Button
    component.color = '#FFFFFF'
  } else if (component instanceof Select) {
    // Select
    component.options = []
  } else {
    // never
    console.log('Тип компонента не определен', typeof component)
  }
}

С помощью предиката с is можно составлять множество других проверок, например, в связке с возможностями JavaScript:

function isSelect(component: Component): component is Select {
 return Array.isArray((component as Select).options)
}

console.log(isSelect(new Select(2, "Селект 1", ["Первый", "Второй", "Расчет окончен!"])))
// true

Type Utils – пишем свои дженерики без головной боли

Используя все рассмотренные возможности языка TS, можно писать свои Type Utils – некоторые вы скорее всего уже использовали в своей работе, например, Pick, Omit или Partial. Но, согласитесь, приятнее написать свои инструменты для создания типов на основе уже существующих. Рассмотрим создание пары таких утилит, например, которые перечислены выше.

  • Pick – утилита для создания типа со списком перечисленных полей:

    • Первым параметром в дженерике будет тип, из которого будем извлекать поля (T), вторым – список (перечисление, union) полей для извлечения (K).

    • Будет удобно сразу ограничить перечисленный список полей – только из тех, которые являются полями исходного типа:

type CustomPick<T, K extends keyof T> = …

Так как union K уже ограничен, то достаточно перебрать все значения K с помощью Mapped Types и подставить в T, чтобы получить значение T[Key]:

type CustomPick<T, K extends keyof T> = { [Key in K]: T[Key] }

// Проверим:

type TButton = {
 id: number,
 label: string,
 color: string,
}

type TButtonWithoutColor = CustomPick<TButton, 'id' | 'label'>
// type TButtonWithoutColor = {
//   id: number;
//   label: string;
// }
  • Omit – тип-утилита, которая удаляет из переданного типа указанный список полей. По сути, это противоположность вышеупомянутому Pick.

    Рассмотрим ее создание по порядку:

    • Перед тем, как перейти к решению, нужно рассмотреть отдельную возможность TypeScript 4.1 версии – ремаппинг (remapping) ключей. Обычно она используется в Mapped Types для переназначения имени ключа с помощью ключевого слова as:

type CustomOmit<T, K extends keyof T> = { [Key in keyof T as Key]: T[Key] }

type TButtonOmitColor = CustomOmit<TButton, 'color'>

const buttonOmitColor: TButtonOmitColor = { id: 1, label: 'Кнопка' }
// Property 'color' is missing in type '{ id: number; label: string; }' but required in type 'CustomOmit<TButton, "color">'

Пока наша утилита не работает, но увидели работу ремаппинга в действии.

  • Добавим условие, по которому будут фильтроваться поля: если ключ входит в перечисление K, то назначим never вместо ключа. В противном случае – оставим ключ в исходном виде. Зачем? Ключи со значением never в тип не попадут, и это можно использовать как фильтр:

type CustomOmit<T, K extends keyof T> = { [Key in keyof T as Key extends K ? never : Key]: T[Key] }

type TButtonOmitColor = CustomOmit<TButton, 'color'>

const buttonOmitColor: TButtonOmitColor = { id: 1, label: 'Кнопка' }
// готово!
  • Partial – тип-утилита, в которой все поля могут быть опциональными. Тут все просто – используем Mapped Types для перебора ключей и добавляем оператор ? для обозначения опциональности поля:

type CustomPartial<T> = { [Key in keyof T]?: T[Key] }

type TButtonPartial = CustomPartial<TButton>

// можно вообще не добавлять поля - теперь они все опциональны
const buttonEmpty: TButtonPartial = { id: 0 }

Более интересный пример – это создание PartialByKeys, в котором второй параметр обозначает набор опциональных полей. Для этого вспомним несколько моментов, которые упоминались выше:

  • Вскользь упоминалось пересечение типов, операция, при которой новый тип будет состоять из всех полей указанных типов, например:

type IntersectionType = { field1: string } & { field2?: string }

// здесь field2 есть...
const intersectedFields: IntersectionType = { field1: 'Обязательное поле 1', field2: 'Необязательное поле 2' }

// ... а тут его нет, так тоже можно :)
const intersectedFieldsSequel: IntersectionType = { field1: 'Обязательное поле 1' }

Обратите внимание, для этого используется оператор &.

  • Чтобы получить тип, в котором перечисленные поля будут опциональными, а остальные останутся обязательными, создадим 2 отдельных типа, после чего применим к ним пересечение. Чтобы лучше понять код ниже, посмотрите создание Omit, часть наших наработок выше используется именно здесь.

type TButton = {
 id: number,
 label: string,
 color: string,
}

type CustomPartialByKeys<T, K extends keyof T> = {
 [Key in keyof T as Key extends K ? Key : never]?: T[Key]
} & {
 [Key in keyof T as Key extends K ? never : Key]: T[Key]
}

type TButtonPartialByKeys = CustomPartialByKeys<TButton, 'label' | 'color'>

const buttonPartial: TButtonPartialByKeys = { id: 0, label: 'Кнопка' }

Что произошло: первый тип в пересечении содержит только опциональные поля из K, второй – только обязательные (это же Omit!). Итог – label и color стали опциональными, что и требовалось сделать.

Что же по итогу?

Мы рассмотрели далеко не все возможности TypeScript, но их вполне достаточно, чтобы писать мощные инструменты для использования в своих проектах. Эти возможности назвать своего рода “базовыми продвинутыми”, и мы, команды НЛМК-ИТ, активно их используем в своих проектах. 

Если вам понравилось, то можно написать и вторую часть о интересных фишках TypeScript, что думаете? Делитесь своими кейсами использования возможностей Typescript в комментариях, посмотрим сколько нас, типизированных frontend-разработчиков :)

Теги:
Хабы:
+6
Комментарии1

Публикации

Информация

Сайт
nlmk.com
Дата регистрации
Дата основания
2013
Численность
свыше 10 000 человек
Местоположение
Россия