slug | title | description | authors | tags | image | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
ts-features |
Полезные возможности современного TypeScript |
Заметка о полезных возможностях современного TypeScript |
harryheman |
|
Привет, друзья!
В данной заметке я расскажу вам о некоторых полезных возможностях современного TypeScript
.
Тип unknown
является безопасной (с точки зрения типов) версией типа any
.
Начнем с того, что между ними общего.
Переменным с типами any
и unknown
можно присваивать любые значения:
let anyValue: any = 'any value'
anyValue = true
anyValue = 123
console.log(anyValue) // 123
let unknownValue: unknown
unknownValue = 'unknown value'
unknownValue = true
unknownValue = 123
console.log(unknownValue) // 123
Однако, в отличие от any
, unknown
не позволяет оперировать "неизвестными" значениями.
Пример:
const anyValue: any = 'any value'
console.log(anyValue.add())
Еще один:
const anyValue: any = true
anyValue.typescript.will.not.complain.about.this.method()
В обоих случаях получаем ошибку времени выполнения (только во время выполнения кода).
При использовании any
мы говорим компилятору: "Не проверяй этот код - я знаю, что делаю". Компилятор TS
, в свою очередь, не делает никаких предположений относительно типа. Это не позволяет предотвращать ошибки на этапе компиляции кода.
Тип unknown
позволяет заранее обнаруживать ошибки времени выполнения (на стадии компиляции). Более того, многие IDE
обеспечивают подсветку потенциальных ошибок такого родп.
Рассмотрим несколько примеров:
let unknownValue: unknown
unknownValue = 'unknown value'
unknownValue.toString() // Ошибка: Object is of type 'unknown'.
unknownValue = 100
const value = unknownValue + 10 // Ошибка: Object is of type 'unknown'.
unknownValue = console
unknownValue.log('test') // Ошибка: Object is of type 'unknown'.
При выполнении любой операции над неизвестным значением возникает ошибка.
Для того, чтобы манипулировать таким значением, требуется произвести сужение типа (type narrowing). Это можно сделать несколькими способами.
С помощью утверждения типа (type assertion):
let unknownValue: unknown
unknownValue = function () {}
;(unknownValue as Function).call(null)
С помощью предохранителя типа (type guard):
let unknownValue: unknown
unknownValue = 'unknown value'
if (typeof unknownValue === 'string') unknownValue.toString()
С помощью кастомного предохранителя типа:
let unknownValue: unknown
type User = { username: string }
function isUser(maybeUser: any): maybeUser is User {
return 'username' in maybeUser
}
unknownValue = { username: 'John' }
if (isUser(unknownValue)) {
console.log(unknownValue.username)
}
С помощью функции-утверждения (assertion function):
let unknownValue: unknown
unknownValue = 123
function isNumber(value: unknown): asserts value is number {
if (typeof value !== 'number') throw Error('Переданное значение не является числом!')
}
isNumber(unknownValue)
unknownValue.toFixed()
Индексированный тип доступа (indexed access type) может использоваться для поиска определенного свойства в другом типе. Рассмотрим пример:
type User = {
id: number
username: string
email: string
address: {
city: string
state: string
country: string
postalCode: number
}
addons: { name: string, id: number }[]
}
type Id = User['id'] // number
type Session = User['address']
// {
// city: string
// state: string
// country: string
// postalCode: string
// }
type Street = User['address']['state'] // string
type Addons = User['addons'][number]
// {
// name: string
// id: number
// }
Здесь мы создаем новые типы Id
, Session
, Street
и Addons
на основе существующего объекта. Индексированный тип доступа можно использовать прямо в функции:
function printAddress(address: User['address']): void {
console.log(`
city: ${address.city},
state: ${address.state},
country: ${address.country},
postalCode: ${address.postalCode}
`)
}
Ключевое слово infer
позволяет выводить один тип из другого внутри условного типа. Пример:
const User = {
id: 123,
username: 'John',
email: '[email protected]',
addons: [
{ name: 'First addon', id: 1 },
{ name: 'Second addon', id: 2 }
]
}
type UnpackArray<T> = T extends (infer R)[] ? R : T
type AddonType = UnpackArray<typeof User.addons> // { name: string, id: number }
Тип addon
выделен в отдельный тип.
Существует специальный набор функций, выбрасывающих исключения, когда происходит что-либо неожиданное. Такие функции называются "функциями-утверждениями" (assertion functions):
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== 'string') throw Error('Переданное значение не является строкой!')
}
Пример проверки объектов, включая вложенные:
type User = {
id: number
username: string
email: string
address: {
city: string
state: string
country: string
postalCode: number
}
}
function assertIsObject(obj: unknown, errorMessage: string = 'Неправильный объект!'): asserts obj is object {
if(typeof obj !== 'object' || obj === null) throw new Error(errorMessage)
}
function assertIsAddress(address: unknown): asserts address is User['address'] {
const errorMessage = 'Неправильны адрес!'
assertIsObject(address, errorMessage)
if(
!('city' in address) ||
!('state' in address) ||
!('country' in address) ||
!('postalCode' in address)
) throw new Error(errorMessage)
}
function assertIsUser(user: unknown): asserts user is User {
const errorMessage = 'Неправильный пользователь!'
assertIsObject(user, errorMessage)
if(
!('id' in user) ||
!('username' in user) ||
!('email' in user)
) throw new Error(errorMessage)
assertIsAddress((user as User).address)
}
Пример использования:
class UserWebService {
static getUser = (id: number): User | unknown => undefined
}
const user = UserWebService.getUser(123)
assertIsUser(user) // Если пользователь является "неправильным", выбрасывается исключение
user.address.postalCode // На данном этапе мы знаем, что `user` - валидный объект
Тип never
является индикатором того, что функция выбрасывает исключение или прерывает выполнение программы:
function plus1If1(value: number): number | never {
if(value === 1) return value + 1
throw new Error('Ошибка!')
}
Пример с промисом:
const promise = (value: number) => new Promise<number | never>((resolve, reject) => {
if(value === 1) resolve(1 + value)
reject(new Error('Ошибка!'))
})
И еще один:
function infiniteLoop(): never {
while (true) {
// ...
}
}
never
также возникает в случае, когда в объединение не осталось "свободных" типов:
function valueCheck(value: string | number) {
if (typeof value === "string") {
// значение является строкой
} else if (typeof value === "number") {
// значение является числом
} else {
// значение имеет тип `never`
}
}
Использование const
при конструировании новых буквальных выражений (literal expressions) сообщает компилятору следующее:
- типы выражения не должны расширяться (например, тип
привет
не может конвертироваться вstring
); - свойства объектных литералов становятся доступными только для чтения (
readonly
); - литералы массивов становятся доступными только для чтения кортежами (tuples).
Пример:
const email = '[email protected]' // [email protected]
const phones = [89876543210, 89123456780] // number[]
const session = { id: 123, name: 'qwerty123456' }
// {
// id: number
// name: string
// }
const username = 'John' as const // John
const roles = [ 'read', 'write'] as const // readonly ["read", "write"]
const address = { street: 'Tokyo', country: 'Japan' } as const
// {
// readonly street: "Tokyo"
// readonly country: "Japan"
// }
const user = {
email,
phones,
session,
username,
roles,
address
} as const
// {
// readonly email: "[email protected]"
// readonly phones: number[]
// readonly session: {
// id: number
// name: string
// }
// readonly username: "John"
// readonly roles: readonly ["read", "write"]
// readonly address: {
// readonly street: "Tokyo"
// readonly country: "Japan"
// }
// }
// С утверждением `const`
// user.email = '[email protected]' // Ошибка
// user.phones = [] // Ошибка
user.phones.push(89087654312)
// user.session = { name: 'test4321', id: 124 } // Ошибка
user.session.name = 'test4321'
// С "внешним" и "внутренним" утверждениями `const`
// user.username = 'Jane' // Ошибка
// user.roles.push('ReadAndWrite') // Ошибка
// user.address.city = 'Osaka' // Ошибка
Утверждение const
позволяет преобразовывать массив строк в объединение:
const roles = ['read', 'write', 'readAndWrite'] as const
type Roles = typeof roles[number]
// Эквивалентно
// type Roles = "read" | "write" | "readAndWrite"
type RolesInCapital = Capitalize<typeof roles[number]>
// Эквивалентно
// type RolesInCapital = "Read" | "Write" | "ReadAndWrite"
[number]
указывает TS
извлечь все числовые индексированные значения из массива roles
.
Ключевое слово override
может использоваться для обозначения перезаписываемого метода дочернего класса (начиная с TS 4.3
):
class Employee {
doWork() {
console.log('Я работаю')
}
}
class Developer extends Employee {
override doWork() {
console.log('Я программирую')
}
}
Вот как сделать эту "фичу" обязательной:
{
"compilerOptions": {
"noImplicitOverride": true
}
}
static
позволяет определять блоки инициализации классов (начиная с TS 4.4
):
function userCount() {
return 10
}
class User {
id = 0
static count: number = 0
constructor(
public username: string,
public age: number
) {
this.id = ++User.count
}
static {
User.count += userCount()
}
}
console.log(User.count) // 10
new User('John', 32)
new User('Jane', 23)
console.log(User.count) // 12
Сигнатура индекса (index signature) позволяет устанавливать больше свойств, чем изначально определено в типе:
class User {
username: string
age: number
constructor(username: string,age: number) {
this.username = username
this.age = age
}
[propName: string]: string | number
}
const user = new User('John', 23)
user['phone'] = '+79876543210'
const something = user['something']
Пример использования сигнатуры статического индекса:
class User {
username: string
age: number
constructor(username: string,age: number) {
this.username = username
this.age = age
}
static [propName: string]: string | number
}
User['userCount'] = 0
const something = User['something']
Благодарю за внимание и happy coding!