Здесь будут рассмотрены шесть продвинутых советов по TypeScript, а также примеры, показывающие, как каждый из них работает шаг за шагом, и их преимущества. Используя эти советы в своем собственном коде TypeScript, вы не только повысите общий уровень своего письма, но и улучшите свои навыки программиста TypeScript.
1 — Расширенные типы
Вы можете создавать новые типы на основе существующих, используя расширенные типы TypeScript, такие как сопоставленные типы и условные типы. С помощью этих типов вы можете изменять типы и манипулировать ими, делая ваш код более гибким и удобным в сопровождении.
Сопоставленные типы
Сопоставленные типы перебирают свойства существующего типа и применяют преобразование для создания нового типа. Одним из распространенных вариантов использования является создание версии типа только для чтения.
type Readonly<T> = { readonly [P in keyof T]: T[P]; }; interface Point { x: number; y: number; } type ReadonlyPoint = Readonly<Point>;
В этом примере мы определяем сопоставленный тип с именем Readonly
, который принимает тип T
в качестве универсального параметра и делает все его свойства доступными только для чтения. Затем мы создаем тип ReadonlyPoint
на основе интерфейса Point
, где все свойства доступны только для чтения.
Условные типы
Условные типы позволяют создавать новый тип на основе условия. Синтаксис подобен тернарному оператору, использующему ключевое слово extends
в качестве ограничения типа.
type NonNullable<T> = T extends null | undefined ? never : T;
В этом примере мы определяем условный тип с именем NonNullable
, который принимает тип T
и проверяет, является ли он расширением null
или undefined
. Если это так, результирующий тип будет never
, в противном случае это исходный тип T
.
Давайте расширим пример с расширенными типами, включив в него больше удобства использования и выходных данных.
interface Point { x: number; y: number; } type ReadonlyPoint = Readonly<Point>; const regularPoint: Point = { x: 5, y: 10 }; const readonlyPoint: ReadonlyPoint = { x: 20, y: 30 }; regularPoint.x = 15; // This works as 'x' is mutable in the 'Point' interface console.log(regularPoint); // Output: { x: 15, y: 10 } // readonlyPoint.x = 25; // Error: Cannot assign to 'x' because it is a read-only property console.log(readonlyPoint); // Output: { x: 20, y: 30 } function movePoint(p: Point, dx: number, dy: number): Point { return { x: p.x + dx, y: p.y + dy }; } const movedRegularPoint = movePoint(regularPoint, 3, 4); console.log(movedRegularPoint); // Output: { x: 18, y: 14 } // const movedReadonlyPoint = movePoint(readonlyPoint, 3, 4); // Error: Argument of type 'ReadonlyPoint' is not assignable to parameter of type 'Point'
В этом примере мы демонстрируем использование сопоставленного типа Readonly
и то, как он обеспечивает неизменяемость. Мы создаем изменяемый объект Point
и объект ReadonlyPoint
только для чтения. Мы показываем, что попытка изменить свойство, доступное только для чтения, приводит к ошибке времени компиляции. Мы также показываем, что типы только для чтения нельзя использовать там, где ожидаются изменяемые типы, что предотвращает непреднамеренные побочные эффекты в нашем коде.
2 — Декораторы
Декораторы в TypeScript — это мощная функция, позволяющая добавлять метаданные, изменять или расширять поведение классов, методов, свойств и параметров. Это функции более высокого порядка, которые можно использовать для наблюдения, изменения или замены определений классов, определений методов, определений средств доступа, определений свойств или определений параметров.
Декораторы класса
Декораторы класса применяются к конструктору класса и могут использоваться для изменения или расширения определения класса.
function LogClass(target: Function) { console.log(`Class ${target.name} was defined.`); } @LogClass class MyClass { constructor() {} }
В этом примере мы определяем декоратор класса с именем LogClass
, который регистрирует имя декорированного класса при его определении. Затем мы применяем декоратор к классу MyClass
, используя синтаксис @
.
Декораторы методов
Декораторы методов применяются к методу класса и могут использоваться для изменения или расширения определения метода.
function LogMethod(target: any, key: string, descriptor: PropertyDescriptor) { console.log(`Method ${key} was called.`); } class MyClass { @LogMethod myMethod() { console.log("Inside myMethod."); } } const instance = new MyClass(); instance.myMethod();
В этом примере мы определяем декоратор метода с именем LogMethod
, который регистрирует имя декорированного метода при его вызове. Затем мы применяем декоратор к методу myMethod
класса MyClass
, используя синтаксис @
.
Декораторы недвижимости
Декораторы свойств применяются к свойству класса и могут использоваться для изменения или расширения определения свойства.
function DefaultValue(value: any) { return (target: any, key: string) => { target[key] = value; }; } class MyClass { @DefaultValue(42) myProperty: number; } const instance = new MyClass(); console.log(instance.myProperty); // Output: 42
В этом примере мы определяем декоратор свойства с именем DefaultValue
, который устанавливает значение по умолчанию для декорированного свойства. Затем мы применяем декоратор к свойству myProperty
класса MyClass
, используя синтаксис @
.
Декораторы параметров
Декораторы параметров применяются к параметру метода или конструктора и могут использоваться для изменения или расширения определения параметра.
function LogParameter(target: any, key: string, parameterIndex: number) { console.log(`Parameter at index ${parameterIndex} of method ${key} was called.`); } class MyClass { myMethod(@LogParameter value: number) { console.log(`Inside myMethod with value ${value}.`); } } const instance = new MyClass(); instance.myMethod(5);
В этом примере мы определяем декоратор параметров с именем LogParameter
, который регистрирует индекс и имя декорированного параметра при вызове метода. Затем мы применяем декоратор к параметру value
метода myMethod
класса MyClass
, используя синтаксис @
.
3 — Пространства имен
Пространства имен в TypeScript — это способ организации и группировки связанного кода. Они помогают избежать конфликтов имен и способствуют модульности за счет инкапсуляции кода, который связан друг с другом. Пространства имен могут содержать классы, интерфейсы, функции, переменные и другие пространства имен.
Определение пространств имен
Чтобы определить пространство имен, используйте ключевое слово namespace
, за которым следует имя пространства имен. Затем вы можете добавить любой связанный код внутри фигурных скобок.
namespace MyNamespace { export class MyClass { constructor(public value: number) {} displayValue() { console.log(`The value is: ${this.value}`); } } }
В этом примере мы определяем пространство имен с именем MyNamespace
и добавляем в него класс MyClass
. Обратите внимание, что мы используем ключевое слово export
, чтобы сделать класс доступным за пределами пространства имен.
Использование пространств имен
Чтобы использовать код из пространства имен, вы можете либо использовать полное имя, либо импортировать код с помощью импорта пространства имен.
// Using the fully-qualified name const instance1 = new MyNamespace.MyClass(5); instance1.displayValue(); // Output: The value is: 5 // Using a namespace import import MyClass = MyNamespace.MyClass; const instance2 = new MyClass(10); instance2.displayValue(); // Output: The value is: 10
В этом примере мы демонстрируем два способа использования класса MyClass
из пространства имен MyNamespace
. Во-первых, мы используем полное имя MyNamespace.MyClass
. Во-вторых, мы используем оператор импорта пространства имен, чтобы импортировать класс MyClass
и использовать его с более коротким именем.
Вложенные пространства имен
Пространства имен могут быть вложены друг в друга для создания иерархии и дальнейшей организации кода.
namespace OuterNamespace { export namespace InnerNamespace { export class MyClass { constructor(public value: number) {} displayValue() { console.log(`The value is: ${this.value}`); } } } } // Using the fully-qualified name const instance = new OuterNamespace.InnerNamespace.MyClass(15); instance.displayValue(); // Output: The value is: 15
В этом примере мы определяем вложенное пространство имен с именем InnerNamespace
внутри файла OuterNamespace
. Затем мы определяем класс MyClass
внутри вложенного пространства имен и используем его с полным именем OuterNamespace.InnerNamespace.MyClass
.
4 — Миксины
Примеси в TypeScript — это способ составления классов из нескольких более мелких частей, называемых классами примесей. Они позволяют повторно использовать и совместно использовать поведение между различными классами, способствуя модульности и возможности повторного использования кода.
Определение миксинов
Чтобы определить класс миксина, создайте класс, который расширяет параметр универсального типа с помощью сигнатуры конструктора. Это позволяет комбинировать класс mixin с другими классами.
class TimestampMixin<TBase extends new (...args: any[]) => any>(Base: TBase) { constructor(...args: any[]) { super(...args); } getTimestamp() { return new Date(); } }
В этом примере мы определяем класс миксина с именем TimestampMixin
, который добавляет метод getTimestamp
, который возвращает текущую дату и время. Класс mixin расширяет параметр универсального типа TBase
сигнатурой конструктора, чтобы его можно было комбинировать с другими классами.
Использование миксинов
Чтобы использовать класс миксина, определите базовый класс и примените к нему класс миксина, используя ключевое слово extends
.
class MyBaseClass { constructor(public value: number) {} displayValue() { console.log(`The value is: ${this.value}`); } } class MyMixedClass extends TimestampMixin(MyBaseClass) { constructor(value: number) { super(value); } }
В этом примере мы определяем базовый класс с именем MyBaseClass
с методом displayValue
. Затем мы создаем новый класс с именем MyMixedClass
, который расширяет базовый класс и применяет к нему класс миксина TimestampMixin
.
Давайте продемонстрируем, как класс mixin работает на практике.
const instance = new MyMixedClass(42); instance.displayValue(); // Output: The value is: 42 const timestamp = instance.getTimestamp(); console.log(`The timestamp is: ${timestamp}`); // Output: The timestamp is: [current date and time]
В этом примере мы создаем экземпляр класса MyMixedClass
, который включает в себя как метод displayValue
из класса MyBaseClass
, так и метод getTimestamp
из класса миксинов TimestampMixin
. Затем мы вызываем оба метода и отображаем их результаты.
5 — Типовая защита
Защита типов в TypeScript — это способ сузить тип переменной или параметра в определенном блоке кода. Они позволяют различать разные типы и получать доступ к свойствам или методам, характерным для этих типов, повышая безопасность типов и снижая вероятность ошибок во время выполнения.
Определение защиты типа
Чтобы определить защиту типа, создайте функцию, которая принимает переменную или параметр и возвращает предикат типа. Предикат типа — это логическое выражение, которое сужает тип параметра в рамках функции.
function isString(value: any): value is string { return typeof value === "string"; }
В этом примере мы определяем функцию защиты типа isString
, которая проверяет, относится ли заданное значение к типу string
. Функция возвращает предикат типа value is string
, который сужает тип параметра value
в рамках функции.
Использование защиты типа
Чтобы использовать защиту типа, просто вызовите функцию защиты типа в условном операторе, таком как оператор if
или оператор switch
.
function processValue(value: string | number) { if (isString(value)) { console.log(`The length of the string is: ${value.length}`); } else { console.log(`The square of the number is: ${value * value}`); } }
В этом примере мы определяем функцию с именем processValue
, которая принимает значение типа string | number
. Мы используем функцию защиты типа isString
, чтобы проверить, является ли значение строкой. Если это так, мы получаем доступ к свойству length
, характерному для типа string
. В противном случае мы предполагаем, что значение равно number
, и вычисляем его квадрат.
Давайте продемонстрируем, как работает type guard на практике.
processValue("hello"); // Output: The length of the string is: 5 processValue(42); // Output: The square of the number is: 1764
В этом примере мы вызываем функцию processValue
со строкой и числом. Функция защиты типа isString
обеспечивает выполнение соответствующего блока кода для каждого типа, что позволяет нам получать доступ к специфичным для типа свойствам и методам без каких-либо ошибок типа.
6 — Типы утилит
Служебные типы в TypeScript предоставляют удобный способ преобразования существующих типов в новые типы. Они позволяют создавать более сложные и гибкие типы без необходимости определять их с нуля, способствуя повторному использованию кода и безопасности типов.
Использование служебных типов
Чтобы использовать служебный тип, примените служебный тип к существующему типу, используя синтаксис угловых скобок. TypeScript предоставляет множество встроенных типов утилит, таких как Partial
, Readonly
, Pick
и Omit
.
interface Person { name: string; age: number; email: string; } type PartialPerson = Partial<Person>; type ReadonlyPerson = Readonly<Person>; type NameAndAge = Pick<Person, "name" | "age">; type WithoutEmail = Omit<Person, "email">;
В этом примере мы определяем интерфейс с именем Person
с тремя свойствами: name
, age
и email
. Затем мы используем различные встроенные типы утилит для создания новых типов на основе интерфейса Person
.
Давайте продемонстрируем, как служебные типы работают на практике.
Частично:
const partialPerson: PartialPerson = { name: "John Doe", };
В этом примере мы создаем объект partialPerson
типа PartialPerson
. Тип утилиты Partial
делает все свойства интерфейса Person
необязательными, что позволяет нам создать частичную персону только со свойством name
.
Только чтение:
const readonlyPerson: ReadonlyPerson = { name: "Jane Doe", age: 30, email: "[email protected]", }; // readonlyPerson.age = 31; // Error: Cannot assign to 'age' because it is a read-only property
В этом примере мы создаем объект readonlyPerson
типа ReadonlyPerson
. Тип утилиты Readonly
делает все свойства интерфейса Person
доступными только для чтения, не позволяя нам изменять свойство age
.
Выбрать:
const nameAndAge: NameAndAge = { name: "John Smith", age: 25, }; // nameAndAge.email; // Error: Property 'email' does not exist on type 'Pick<Person, "name" | "age">'
В этом примере мы создаем объект nameAndAge
типа NameAndAge
. Тип утилиты Pick
создает новый тип только с указанными свойствами интерфейса Person
, в данном случае name
и age
.
Пропустить:
const withoutEmail: WithoutEmail = { name: "Jane Smith", age: 28, }; // withoutEmail.email; // Error: Property 'email' does not exist on type 'Omit<Person, "email">'
В этом примере мы создаем объект withoutEmail
типа WithoutEmail
. Тип утилиты Omit
создает новый тип, удаляя указанные свойства из интерфейса Person
, в данном случае email
.
В заключение в этой статье были рассмотрены различные расширенные темы TypeScript, такие как пространства имен, расширенные типы, декораторы, примеси, защита типов и служебные типы. Понимая и используя эти функции, вы можете создавать более модульный, повторно используемый и удобный в сопровождении код, который соответствует лучшим практикам и снижает вероятность ошибок во время выполнения.
Используя эти расширенные функции TypeScript, вы можете писать более чистый, организованный и удобный для сопровождения код, который в полной мере использует мощную систему типов и языковые функции TypeScript.
Если вам понравилась эта статья и вы нашли ее полезной, не стесняйтесь ознакомиться с другой моей статьей 12 — Советы по TypeScript для чистого кода. Расширьте свои знания TypeScript и улучшите свои навыки кодирования, изучив дополнительные советы и приемы!