Typecasting
Typecasting - привидение типа (англ. type conversion) — это преобразование значения одного типа в значение другого типа. В Javascript (ECMAScript) это делается автоматически по необходимости с использованием вспомогательных методов toString() и valueOf().
TypeScript вы, скорее всего, используете именно из-за его основной фичи: статическая типизация. Так как TypeScript - это все же тот же Javascript, а вы, скорее всего, Frontend разработчик, то вы не можете жить с переменными строго одного типа. И ваш код пестрит различными мультитипами или периодически вы пытаетесь приводить один тип к другому. Иногда это усложняет вам работу, так как вы можете наталкиваться на "непонятное" для вас поведение компилятора.
Простой пример: вы захотели иметь переменную которая может быть строкой либо массивом строк. Вы считаете что это ок и вам ну очень нужно такое поведение. Окей, TypeScript позволяет описать такой тип следующим образом:
type arOrStr = string[] | string;
let someVar :arOrStr = 'abc';
someVar = 123;
Вроде бы все логично, определили перменную которая или строка или массив строк. Все корректно. Теперь рассмотрим пример некоторой логики, где вы можете использовать такой тип:
type arOrStr = string[] | string;
function foo(a :arOrStr) {
if (typeof a === 'string') {
a = [a];
}
a.map( x => x.toUpperCase() );
}
foo('abcdef');
foo(['abc', 'def']);
Ожидаемая логика: если пришла строка, то я преобразую переменную в массив из одного элемента с этой строкой. Но вот компилятор упорно ругается. Во-первых, он говорит вам, что есть несоответствие типов (хотя вы-то думаете что все логично):
Error:(9, 3) TS2322: Type '(string[] | string)[]' is not assignable to type 'string[] | string'. Type '(string[] | string)[]' is not assignable to type 'string'.
Во-вторых, он говорит вам, что у перменной данного типа нет метода map(). НО как же так? Ведь у массива есть такой тип. Вот незадача. А что тут не так? На самом деле компилятор делает вам добро! Посмотрите на код внимательнее, тут нет защиты от того, что в рантайме не придет вместо строки что-то другое. Проверка типов только на стадии компиляции, но не на стадии выполнения. В таком случае вы можете сказать, что тогда можно переписать код таким образом:
function foo(a :arOrStr) {
if (typeof a === 'string') {
a = [a];
}
// Гарантируем, что здесь будет только 100% массив и ничто другое
if (Array.isArray(a)) {
a.map(x => x.toUpperCase());
}
}
Все, теперь компилятор знает про метод map() в нашей перменной, так как мы ему гарантируем что в данном случае у нас будет массив. Но он продолжает ругаться на то, что нельзя присвоить a = [a]
. Почему?
Потому, что если расписать данную ситуация через описания типов, то мы получаем следующую картину:
var a :arOrStr[]
что можно переписать как
var a :(string[] | string) []
или
var a: Array<string[] | string>
Т.е. у нас идет попытка присвоить результат другого типа. И с точки зрения компилятора все логично и верно. Это мы, армия JS разработчиков, привыкли что можно так делать и это одно и тоже. Нет, с точки зрения статической типизации - это разные типы. И так, что делать?
Самый простой способ - это завести новую перменную другого типа (проще any) и уже ей присвоить результат:
function foo(a :arOrStr) {
let b :string[];
if (!Array.isArray(a)) {
b = [a];
}
else {
b = a;
}
b.map(x => x.toUpperCase());
}
Обратит внимание, в таком описании нам уже не надо помещать вызов b.map() в условие. Но тут появляются риски ошибок в рантайме. На уровне статического анализа компилятор не позволит просунуть внутрь функции что-то отличное от массива и строки, но в рантайме... Так что это не самый лучший способ.
Кастуем типы
Те, кто разрабатывали или пишут на PHP, знают, что там есть такие вот конструкции:
<?php
$a = "123abc";
$b = (int) $a;
В данном случае мы произвели приведение типа (кастинг). В TypeScript есть такой же способ кастовать типы. Теперь перепишем наш пример с применением преобразования:
function foo(a :string[] | string) {
if (!Array.isArray(a)) {
a = <string[]> [a];
}
a.map( x => x.toUpperCase() );
}
Теперь компилятор пропускает запись переопределения перменной a=[a]
. Но вне этого условия статический анализатор ругается на то, что у нас перменная a такого типа, у которого не может быть метода map(), хоть выше мы явно указали, что если пришло значение отличное от массива, то всеравно преобразуем в массив. В данном случае есть два пути выхода. Первый, добавить еще одну проверку на то, что у нас массив:
function foo(a :string[] | string) {
if (!Array.isArray(a)) {
a = <string[]> [a];
}
if (Array.isArray(a)) {
a.map(x => x.toUpperCase());
}
}
Но в итоге будет скомпилирован код с двумя условиями, а вы точно понимаете, что хоть в рантайме придет все что угодно, но оно всеравно будет приведено к массиву. Хоть это будет и массив чисел или массив объектов, все же это будет массив. Тогда мы можем применить каст перед вызовом метода, просто указав компилятору, что мы понимаем, что мы делаем:
function foo(a :string[] | string) {
if (!Array.isArray(a)) {
a = <string[]> [a];
}
(<string[]> a).map( x => x.toUpperCase() );
}
Если говорить простым языком, мы попросили заткнуться компилятор и верить нам на слово.