Система типов TypeScript устроена так, чтобы быть удобной и допускать нерациональное поведение, например что угодно может быть присвоено для типа any, что означает сказать компилятору разрешить вам делать все, что вы захотите:
12345
letfoo:any=123;foo='Hello';// Позжеfoo.toPrecision(3);// Разрешено, потому что вы описали foo как `any`
Объекты TypeScript структурно типизированы. Это означает, что имена типов не имеют значения, пока структуры совпадают.
1 2 3 4 5 6 7 8 9101112
interfacePoint{x:number;y:number;}classPoint2D{constructor(publicx:number,publicy:number){}}letp:Point;// всё в порядке из-за структурной типизацииp=newPoint2D(1,2);
Это позволяет вам сходу создавать объекты (как в ванильном JS) и при этом сохранять проверку типов всякий раз, когда это можно логически вывести.
Также лишние данные не считаются ошибкой:
1 2 3 4 5 6 7 8 9101112131415161718
interfacePoint2D{x:number;y:number;}interfacePoint3D{x:number;y:number;z:number;}varpoint2D:Point2D={x:0,y:10};varpoint3D:Point3D={x:0,y:10,z:20};functioniTakePoint2D(point:Point2D){/* сделать что-то */}iTakePoint2D(point2D);// точное совпадение - okayiTakePoint2D(point3D);// дополнительная информация - okayiTakePoint2D({x:0});// Ошибка: отсутствует `y`
Расхождение - это простая и важная для понимания концепция анализа совместимости типов.
Для простых типов Base и Child, если Child является дочерним по отношению к Base, то экземпляры Child могут быть присвоены переменной типа Base.
Это полиморфизм 101
Совместимость сложных типов зависит от расхождения:
Ковариантный: (ко === совместный) только в одном направлении.
Контравариантный: (контра === обратный) только в противоположном направлении.
Бивариантный: (би === оба) как ко, так и контра.
Инвариантный: если типы не совпадают абсолютно полностью, то они несовместимы.
Примечание: для максимально безопасной системы типов при присутствии мутабельности данных, как в JavaScript, инвариантный - единственный правильный вариант. Но, как уже упоминалось, удобство заставляет нас выбирать менее безопасный вариант.
Ковариантный: Тип возвращаемого значения должен содержать хотя бы необходимые данные.
1 2 3 4 5 6 7 8 9101112131415161718
/** Иерархия типов */interfacePoint2D{x:number;y:number;}interfacePoint3D{x:number;y:number;z:number;}/** Два примера-функции */letiMakePoint2D=():Point2D=>({x:0,y:0});letiMakePoint3D=():Point3D=>({x:0,y:0,z:0});/** Присвоение */iMakePoint2D=iMakePoint3D;// OkayiMakePoint3D=iMakePoint2D;// ОШИБКА: Point2D не может быть присвоен Point3D
Допускается меньшее количество параметров (т.е. функции могут игнорировать дополнительные параметры). Ведь они гарантированно вызываются хотя бы с необходимыми параметрами.
1 2 3 4 5 6 7 8 910111213
letiTakeSomethingAndPassItAnErr=(x:(err:Error,data:any)=>void)=>{/* сделать что-то */};iTakeSomethingAndPassItAnErr(()=>null);// OkayiTakeSomethingAndPassItAnErr((err)=>null);// OkayiTakeSomethingAndPassItAnErr((err,data)=>null);// Okay// ОШИБКА: параметр типа '(err: any, data: any, more: any) => null'// не может быть назначен параметру типа '(err: Error, data: any) => void'.iTakeSomethingAndPassItAnErr((err,data,more)=>null);
Необязательные (предварительно определенное количество) и остальные параметры (любое количество параметров) совместимы, опять же для удобства.
1 2 3 4 5 6 7 8 9101112
letfoo=(x:number,y:number)=>{/* сделать что-то */};letbar=(x?:number,y?:number)=>{/* сделать что-то */};letbas=(...args:number[])=>{/* сделать что-то */};foo=bar=bas;bas=bar=foo;
Примечание: необязательные (в нашем примере bar) и обязательные (в нашем примере foo) совместимы, только если strictNullChecks имеет значение false.
/** Иерархия событий */interfaceEvent{timestamp:number;}interfaceMouseEventextendsEvent{x:number;y:number;}interfaceKeyEventextendsEvent{keyCode:number;}/** Пример слушателя событий */enumEventType{Mouse,Keyboard,}functionaddEventListener(eventType:EventType,handler:(n:Event)=>void){/* ... */}// Неидеально, но полезно и распространено. Работает как двувариантная функция// сравнения параметровaddEventListener(EventType.Mouse,(e:MouseEvent)=>console.log(e.x+','+e.y));// Нежелательные альтернативы для достижения идеальностиaddEventListener(EventType.Mouse,(e:Event)=>console.log((<MouseEvent>e).x+','+(<MouseEvent>e).y));addEventListener(EventType.Mouse,<(e:Event)=>void>(((e:MouseEvent)=>console.log(e.x+','+e.y))));// Не допускается (явная ошибка). Проверка типов применена для полностью// несовместимых типовaddEventListener(EventType.Mouse,(e:number)=>console.log(e));
Также делает Array<Child> присваиваемым Array<Base> (ковариационным), поскольку функции совместимы. Ковариационный массив требует, чтобы все функции Array<Child> могли быть присвоены Array<Base>, например push(t:Child) назначается push(t:Base), что стало возможным благодаря двувариантным параметрам функции.
Это может сбивать с толку людей, пришедших из других языков, которые ожидали бы следующей ошибки, но не в TypeScript:
1 2 3 4 5 6 7 8 9101112131415161718192021
/** Иерархия типов */interfacePoint2D{x:number;y:number;}interfacePoint3D{x:number;y:number;z:number;}/** Два примера-функции */letiTakePoint2D=(point:Point2D)=>{/* сделать что-то */};letiTakePoint3D=(point:Point3D)=>{/* сделать что-то */};iTakePoint3D=iTakePoint2D;// Okay : РазумноiTakePoint2D=iTakePoint3D;// Okay : ЧЕГО?
Значения перечислений из разных типов перечислений считаются несовместимыми. Это делает перечисления пригодными для формального использования(в отличие от структурных типов)
Сравниваются только члены экземпляра и методы. конструкторы и статика роли не играют.
1 2 3 4 5 6 7 8 910111213141516171819
classAnimal{feet:number;constructor(name:string,numFeet:number){/** сделать что-то */}}classSize{feet:number;constructor(meters:number){/** сделать что-то */}}leta:Animal;lets:Size;a=s;// OKs=a;// OK
private и protected члены должны происходить из одного класса. Такие члены по сути делают класс именным.
1 2 3 4 5 6 7 8 9101112131415161718192021
/** Иерархия классов */classAnimal{protectedfeet:number;}classCatextendsAnimal{}letanimal:Animal;letcat:Cat;animal=cat;// OKAYcat=animal;// OKAY/** Похож на Animal */classSize{protectedfeet:number;}letsize:Size;animal=size;// ОШИБКАsize=animal;// ОШИБКА
Поскольку TypeScript имеет систему структурных типов, параметры типа влияют на совместимость только когда используются. Например, в следующем примере T не влияет на совместимость:
12345
interfaceEmpty<T>{}letx:Empty<number>;lety:Empty<string>;x=y;// okay, y соответствует структуре x
Однако, если используется T, он будет играть роль в совместимости на основе его конкретизации, как показано ниже:
1234567
interfaceNotEmpty<T>{data:T;}letx:NotEmpty<number>;lety:NotEmpty<string>;x=y;// ошибка, x и y несовместимы
В случаях, когда общие параметры не были созданы, они заменяются на any перед проверкой совместимости:
123456789
letidentity=function<T>(x:T):T{// ...};letreverse=function<U>(y:U):U{// ...};identity=reverse;// Okay, потому что (x: any)=>any совпадает с (y: any)=>any
Обобщения, включающие классы, сопоставляются по совместимости на уровне классов, как мы упоминали ранее. Например:
Мы сказали, что инвариантность - самый разумный вариант. Вот пример, в котором показывается, что контравариантный и ковариантный небезопасны для массивов.
/** Иерархия */classAnimal{constructor(publicname:string){}}classCatextendsAnimal{meow(){}}/** По одному экземпляру каждого */varanimal=newAnimal('animal');varcat=newCat('cat');/** * Демонстрация: полиморфизм 101 * Animal <= Cat */animal=cat;// Okaycat=animal;// ОШИБКА: cat наследуется от animal/** Массив экземпляров каждого для демонстрации расхождения */letanimalArr:Animal[]=[animal];letcatArr:Cat[]=[cat];/** * Очевидно плохо: Контравариантность * Animal <= Cat * Animal[] >= Cat[] */catArr=animalArr;// Okay, если контравариантныйcatArr[0].meow();// Разрешено, но БЭМС 🔫 во время выполнения/** * Также плохо: ковариантный * Animal <= Cat * Animal[] <= Cat[] */animalArr=catArr;// Okay, если ковариантныйanimalArr.push(newAnimal('another animal'));// Просто добавили animal// в catArr!catArr.forEach((c)=>c.meow());// Разрешено, но БЭМС 🔫 во время выполнения