Перейти к содержанию

Проверка типов шаблонов

📅 28.02.2022

Обзор проверки типов шаблонов

Подобно тому, как TypeScript выявляет ошибки типов в коде, Angular проверяет выражения и привязки в шаблонах приложения и может сообщать о найденных ошибках типов. В настоящее время Angular имеет три режима такой проверки, в зависимости от значения флагов fullTemplateTypeCheck и strictTemplates в конфигурационном файле TypeScript.

Базовый режим

В самом базовом режиме проверки типов, когда флаг fullTemplateTypeCheck установлен в значение false, Angular проверяет только выражения верхнего уровня в шаблоне.

Если написать <map [city]="user.address.city">, то компилятор проверит следующее:

  • user является свойством класса компонента
  • user является объектом со свойством address
  • user.address является объектом со свойством city

Компилятор не проверяет, что значение user.address.city может быть присвоено входу city компонента <map>.

В этом режиме компилятор также имеет ряд серьезных ограничений:

  • Важно отметить, что он не проверяет встроенные представления, такие как *ngIf, *ngFor, другие <ng-template> встроенные представления.
  • Не проверяются типы #refs, результаты работы пайпов, тип $event в привязках событий.

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

Полный режим

Если флаг fullTemplateTypeCheck установлен в true, Angular более агрессивно проверяет типы в шаблонах. В частности:

  • Проверяются встроенные представления (например, внутри *ngIf или *ngFor).
  • Пайпы имеют правильный тип возврата
  • Локальные ссылки на директивы и пайпы имеют правильный тип (за исключением любых общих параметров, которые будут иметь тип any)

Следующие параметры все еще имеют тип any.

  • Локальные ссылки на элементы DOM
  • Объект $event
  • Выражения безопасной навигации

Флаг fullTemplateTypeCheck был устаревшим в Angular 13. Вместо него следует использовать семейство опций компилятора strictTemplates.

Строгий режим

Angular сохраняет поведение флага fullTemplateTypeCheck и вводит третий "строгий режим". Строгий режим является супермножеством полного режима, и доступ к нему осуществляется путем установки флага strictTemplates в true.

Этот флаг заменяет флаг fullTemplateTypeCheck.

В строгом режиме Angular использует проверки, которые выходят за рамки проверки типов версии 8.

Строгий режим доступен только при использовании Ivy.

В дополнение к поведению в полном режиме, Angular делает следующее:

  • Проверяет, что привязки компонентов/директив могут быть назначены на их @Input().
  • Подчиняется флагу TypeScript strictNullChecks при проверке предшествующего режима
  • Определяет правильный тип компонентов/директив, включая дженерики
  • Определяет типы контекста шаблона, если это настроено (например, позволяет правильно проверить тип NgFor)
  • Определяет правильный тип $event в привязках компонентов/директив, DOM и анимационных событий
  • Определяет правильный тип локальных ссылок на элементы DOM, основываясь на имени тега (например, тип, который document.createElement вернет для этого тега)

Проверка *ngFor

Три режима проверки типов по-разному относятся к встроенным представлениям. Рассмотрим следующий пример.

1
2
3
4
5
6
7
interface User {
    name: string;
    address: {
        city: string;
        state: string;
    };
}

1
2
3
4
<div *ngFor="let user of users">
  <h2>{{config.title}}</h2>
  <span>City: {{user.address.city}}</span>
</div>

Элементы <h2> и <span> находятся во встроенном представлении *ngFor. В базовом режиме Angular не проверяет ни один из них. Однако в полном режиме Angular проверяет существование config и user и предполагает тип any.

В строгом режиме Angular знает, что user в <span> имеет тип User, и что address — это объект со свойством city типа string.

Устранение ошибок шаблонов

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

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

Также возможны ложные срабатывания, когда типизация библиотеки Angular неполная или неправильная, или когда типизация не совсем соответствует ожиданиям, как в следующих случаях.

  • Когда типизация библиотеки неверна или неполна (например, отсутствует null | undefined, если библиотека не была написана с strictNullChecks в уме).

  • Когда типы ввода библиотеки слишком узкие, и библиотека не добавила соответствующие метаданные, чтобы Angular мог это понять.

    Обычно это происходит при использовании атрибутов disabled или других распространенных булевых входов, например, <input disabled>.

  • При использовании $event.target для событий DOM (из-за возможности пузырения событий, $event.target в типизации DOM не имеет того типа, который вы могли бы ожидать).

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

  • Использовать функцию $any() type-cast function в определенных контекстах, чтобы отказаться от проверки типов для части выражения.

  • Отключить строгую проверку полностью, установив strictTemplates: false в конфигурационном файле TypeScript приложения, tsconfig.json.

  • Отключить определенные операции проверки типов по отдельности, сохраняя строгость в других аспектах, установив флаг строгости в false.

  • Если вы хотите использовать strictTemplates и strictNullChecks вместе, откажитесь от строгой проверки нулевого типа специально для входных привязок, используя strictNullInputTypes.

Если не указано иное, каждый следующий параметр устанавливается в значение для strictTemplates (true, когда strictTemplatestrue и наоборот).

strictInputTypes
Проверяется ли присваиваемость выражения привязки полю @Input(). Также влияет на вывод директивных общих типов.
strictInputAccessModifiers
Учитываются ли модификаторы доступа, такие как private/protected/readonly, при присвоении выражения привязки к @Input(). Если опция отключена, то модификаторы доступа для @Input игнорируются; проверяется только тип. По умолчанию эта опция является false, даже если для strictTemplates установлено значение true.
strictNullInputTypes
Соблюдается ли strictNullChecks при проверке привязок @Input() (согласно strictInputTypes). Отключение этого параметра может быть полезно при использовании библиотеки, созданной без учета strictNullChecks.
strictAttributeTypes
Проверять ли привязки @Input(), выполненные с использованием текстовых атрибутов. Например, <input matInput disabled="true"> (установка свойства disabled в строку 'true') против <input matInput [disabled]="true"> (установка свойства disabled в булево true).
strictSafeNavigationTypes
Указывается ли возвращаемый тип операций безопасной навигации (например, user?.name будет корректно выводиться на основе типа user). Если отключено, то user?.name будет иметь тип any.
strictDomLocalRefTypes
Будут ли локальные ссылки на элементы DOM иметь правильный тип. Если отключено, то ref будет иметь тип any для <input #ref>.
strictOutputEventTypes
Будет ли $event иметь правильный тип для привязки событий к компоненту/директиве an @Output(), или к событиям анимации. Если отключено, то будет любой.
strictDomEventTypes
Будет ли $event иметь правильный тип для привязки к событиям DOM. Если отключено, то это будет any.
strictContextGenerics
Будут ли корректно инфецироваться параметры типа общих компонентов (включая любые общие границы). Если отключено, то любые параметры типа будут иметь значение any.
strictLiteralTypes
Определять ли тип литералов объектов и массивов, объявленных в шаблоне. Если флаг отключен, то тип таких литералов будет any. Этот флаг имеет значение true, если для either fullTemplateTypeCheck или strictTemplates установлено значение true.

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

Если это не сработает, то в крайнем случае можно полностью отключить полный режим с помощью fullTemplateTypeCheck: false.

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

Если это произошло, file an issue, чтобы команда могла решить эту проблему.

Вводы и проверка типов

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
export interface User {
    name: string;
}

@Component({
    selector: 'user-detail',
    template: '{{ user.name }}',
})
export class UserDetailComponent {
    @Input() user: User;
}

Шаблон AppComponent использует этот компонент следующим образом:

1
2
3
4
5
6
7
8
@Component({
    selector: 'app-root',
    template:
        '<user-detail [user]="selectedUser"></user-detail>',
})
export class AppComponent {
    selectedUser: User | null = null;
}

Здесь, во время проверки типа шаблона для AppComponent, привязка [user]="selectedUser" соответствует входу UserDetailComponent.user. Поэтому Angular присваивает свойство selectedUser свойству UserDetailComponent.user, что привело бы к ошибке, если бы их типы были несовместимы.

TypeScript проверяет присвоение в соответствии со своей системой типов, подчиняясь таким флагам, как strictNullChecks, поскольку они настроены в приложении.

Избегайте ошибок типа во время выполнения, предоставляя более конкретные требования к типу в шаблоне для проверки типа шаблона. Сделайте требования к входным типам для ваших собственных директив как можно более конкретными, предоставив функции защиты шаблона в определении директивы.

Смотрите Улучшение проверки типов шаблонов для пользовательских директив в этом руководстве.

Строгие проверки нуля

Когда вы включаете strictTemplates и флаг TypeScript strictNullChecks, в некоторых ситуациях могут возникать ошибки проверки типов, которых нелегко избежать. Например:

  • Нулевое значение, связанное с директивой из библиотеки, в которой не включена strictNullChecks.

    Для библиотеки, скомпилированной без strictNullChecks, ее файлы объявлений не будут указывать, может ли поле быть null или нет.

    Для ситуаций, когда библиотека корректно обрабатывает null, это проблематично, так как компилятор будет проверять значение с нулевым значением на соответствие файлам объявления, в которых тип null опущен.

    Таким образом, компилятор выдает ошибку проверки типов, поскольку он придерживается strictNullChecks.

  • Использование пайпа async с Observable, который, как вы знаете, будет испускаться синхронно.

    Пайп async в настоящее время предполагает, что Observable, на которую она подписывается, может быть асинхронной, что означает, что возможно, что значение еще не доступно.

    В этом случае она все равно должна возвращать что-то — это null.

    Другими словами, тип возврата пайпа async включает null, что может привести к ошибкам в ситуациях, когда известно, что Observable синхронно выдает значение, не являющееся nullable.

Существует два возможных обходных пути решения вышеупомянутых проблем:

  • В шаблоне включите оператор утверждения ! в конце выражения с нулевым значением, например

    1
    <user-detail [user]="user!"></user-detail>
    

    В этом примере компилятор игнорирует несовместимость типов в nullability, как и в коде TypeScript.

    В случае с пайпом async обратите внимание, что выражение необходимо обернуть круглыми скобками, как в примере

    1
    <user-detail [user]="(user$ | async)!"></user-detail>
    
  • Полностью отключить строгую проверку нуля в шаблонах Angular.

    Когда включена опция strictTemplates, все еще можно отключить некоторые аспекты проверки типов.

    Установка опции strictNullInputTypes в false отключает строгую проверку нулевых типов в шаблонах Angular.

    Этот флаг применяется для всех компонентов, которые являются частью приложения.

Советы для авторов библиотек

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

Кроме того, можно предоставлять подсказки типов, специфичные для проверки типов шаблонов.

Смотрите Улучшение проверки типов шаблонов для пользовательских директив, и Коэрцитивность входных сеттеров.

Принуждение входного сеттера

Иногда желательно, чтобы @Input() директивы или компонента изменял привязанное к нему значение, обычно используя пару getter/setter для ввода. В качестве примера рассмотрим компонент пользовательской кнопки:

Рассмотрим следующую директиву:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Component({
    selector: 'submit-button',
    template: `
        <div class="wrapper">
            <button [disabled]="disabled">Submit</button>
        </div>
    `,
})
class SubmitButton {
    private _disabled: boolean;

    @Input()
    get disabled(): boolean {
        return this._disabled;
    }

    set disabled(value: boolean) {
        this._disabled = value;
    }
}

Здесь вход disabled компонента передается на <button> в шаблоне. Все это работает, как и ожидалось, пока к входу привязано значение boolean. Но, предположим, потребитель использует этот вход в шаблоне в качестве атрибута:

1
<submit-button disabled></submit-button>

Это имеет тот же эффект, что и связывание:

1
<submit-button [disabled]="''"></submit-button>

Во время выполнения входное значение будет установлено в пустую строку, которая не является значением типа boolean. Библиотеки компонентов Angular, которые решают эту проблему, часто "принуждают" значение к нужному типу в сеттере:

1
2
3
set disabled(value: boolean) {
  this._disabled = (value === '') || value;
}

Идеально было бы изменить здесь тип value с boolean на boolean|'', чтобы соответствовать набору значений, которые фактически принимаются сеттером. TypeScript до версии 4.3 требует, чтобы и геттер, и сеттер имели одинаковый тип, поэтому если геттер должен вернуть boolean, то сеттер будет иметь более узкий тип.

Если у потребителя включена самая строгая проверка типов шаблонов Angular, это создает проблему: пустая строка ('') не может быть присвоена полю disabled, что создает ошибку типа при использовании формы атрибута.

В качестве обходного пути решения этой проблемы Angular поддерживает проверку более широкого, более допустимого типа для @Input(), чем объявлено для самого поля ввода. Включите эту возможность, добавив в класс компонента статическое свойство с префиксом ngAcceptInputType_:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
class SubmitButton {
    private _disabled: boolean;

    @Input()
    get disabled(): boolean {
        return this._disabled;
    }

    set disabled(value: boolean) {
        this._disabled = value === '' || value;
    }

    static ngAcceptInputType_disabled: boolean | '';
}

Начиная с TypeScript 4.3, сеттер может быть объявлен как принимающий boolean|'' в качестве типа, что делает поле принуждения входного сеттера устаревшим. Таким образом, поля принуждения входных сеттеров были устаревшими.

Это поле не обязательно должно иметь значение. Его существование сообщает системе проверки типов Angular, что вход disabled следует рассматривать как принимающий привязки, соответствующие типу boolean|''.

Суффиксом должно быть имя поля @Input.

Следует обратить внимание на то, что если для данного входа существует переопределение ngAcceptInputType_, то сеттер должен быть способен обрабатывать любые значения переопределенного типа.

Отключение проверки типов с помощью $any()

Отключите проверку выражения привязки, окружив выражение вызовом псевдофункции $any() cast pseudo-function. Компилятор рассматривает это как приведение к типу any, как в TypeScript, когда используется приведение <any> или as any.

В следующем примере приведение person к типу any подавляет ошибку Property address does not exist.

1
2
3
4
5
6
7
@Component({
    selector: 'my-component',
    template: '{{$any(person).address.street}}',
})
class MyComponent {
    person?: Person;
}

Ссылки

Комментарии