From c895929f58b1219b78aac3f667001d07feb04096 Mon Sep 17 00:00:00 2001 From: Edwin van den Houdt Date: Sat, 1 Aug 2026 10:29:47 +0200 Subject: [PATCH] fix: enable strict mode, honest HTTP boundary, and add routing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three fixes to the parts of the template that contradicted its own "illegal states unrepresentable" claim: - tsconfig: turn on strict + strictTemplates (measured zero fallout — the codebase already typechecked cleanly, it just wasn't enforced). - RemoteData drops its unused error type parameter (Resource.error is always Error) and Failure now carries a real Error. Pages read the union with @let instead of re-deriving from the resource, which deletes the non-null assertion strictNullChecks would otherwise flag. - users.adapter.ts never checked response.ok, so an HTTP error resolved as a garbage Success and crashed instead of reaching RemoteData's Failure branch. New shared/infrastructure/http.ts adds the status check plus hand-written parse guards and abortSignal forwarding; the six fetch stubs across the test suite (which encoded the missing check) and adapter spec now cover the Failure and Empty paths. Also adds real routing (@angular/router was a dependency with zero imports and a fake "Back" button): /users and /users/:id are now deep-linkable via withComponentInputBinding(), tested with RouterTestingHarness driving real navigation. Co-Authored-By: Claude Sonnet 5 --- src/app/app.config.ts | 7 +- src/app/app.html | 2 +- src/app/app.routes.ts | 9 ++ src/app/app.spec.ts | 31 ++++-- src/app/app.ts | 5 +- src/app/shared/application/remote-data.ts | 9 +- src/app/shared/infrastructure/http.ts | 34 ++++++ .../ui/molecules/async.component.spec.ts | 4 +- .../shared/ui/molecules/async.component.ts | 2 +- .../users/application/user-detail.resource.ts | 5 +- src/app/users/application/users.resource.ts | 3 +- .../infrastructure/users.adapter.spec.ts | 71 +++++++++++++ src/app/users/infrastructure/users.adapter.ts | 33 +++++- .../organisms/user-detail.component.spec.ts | 4 + .../ui/organisms/user-detail.component.ts | 7 +- src/app/users/ui/users.page.spec.ts | 100 ++++++++++++------ src/app/users/ui/users.page.ts | 19 ++-- src/app/users/ui/users.routes.ts | 7 ++ src/index.html | 20 ++-- tsconfig.app.json | 8 +- tsconfig.json | 2 + tsconfig.spec.json | 9 +- 22 files changed, 301 insertions(+), 90 deletions(-) create mode 100644 src/app/app.routes.ts create mode 100644 src/app/shared/infrastructure/http.ts create mode 100644 src/app/users/infrastructure/users.adapter.spec.ts create mode 100644 src/app/users/ui/users.routes.ts diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 45c753e..340dd7f 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,5 +1,10 @@ import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { routes } from './app.routes'; export const appConfig: ApplicationConfig = { - providers: [provideBrowserGlobalErrorListeners()], + providers: [ + provideBrowserGlobalErrorListeners(), + provideRouter(routes, withComponentInputBinding()), + ], }; diff --git a/src/app/app.html b/src/app/app.html index 4dcec5d..67e7bd4 100644 --- a/src/app/app.html +++ b/src/app/app.html @@ -1 +1 @@ - + diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts new file mode 100644 index 0000000..9efd01f --- /dev/null +++ b/src/app/app.routes.ts @@ -0,0 +1,9 @@ +import { Routes } from '@angular/router'; + +export const routes: Routes = [ + { + path: 'users', + loadChildren: () => import('@users/ui/users.routes').then((m) => m.usersRoutes), + }, + { path: '', redirectTo: 'users', pathMatch: 'full' }, +]; diff --git a/src/app/app.spec.ts b/src/app/app.spec.ts index 85085ab..27e463a 100644 --- a/src/app/app.spec.ts +++ b/src/app/app.spec.ts @@ -1,14 +1,25 @@ -import { describe, expect, it, vi } from 'vitest'; +import { afterEach, describe, expect, it, vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; -import { App } from './app'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { routes } from './app.routes'; -describe('App', () => { - it('renders the users page heading', async () => { - vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ json: () => Promise.resolve([]) })); - await TestBed.configureTestingModule({ imports: [App] }).compileComponents(); - const fixture = TestBed.createComponent(App); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('h1')?.textContent).toBe('Users'); - vi.unstubAllGlobals(); +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('App routes', () => { + it('redirects to /users and renders the page heading', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve([]) }), + ); + TestBed.configureTestingModule({ + providers: [provideRouter(routes, withComponentInputBinding())], + }); + const harness = await RouterTestingHarness.create('/'); + await harness.fixture.whenStable(); + harness.detectChanges(); + expect(harness.routeNativeElement?.querySelector('h1')?.textContent).toBe('Users'); }); }); diff --git a/src/app/app.ts b/src/app/app.ts index 196265f..0f3e17a 100644 --- a/src/app/app.ts +++ b/src/app/app.ts @@ -1,10 +1,9 @@ import { Component } from '@angular/core'; -import { UsersPage } from '@users/ui/users.page'; +import { RouterOutlet } from '@angular/router'; @Component({ selector: 'app-root', - imports: [UsersPage], + imports: [RouterOutlet], templateUrl: './app.html', - styleUrl: './app.css', }) export class App {} diff --git a/src/app/shared/application/remote-data.ts b/src/app/shared/application/remote-data.ts index 3e1c4e3..dc4d740 100644 --- a/src/app/shared/application/remote-data.ts +++ b/src/app/shared/application/remote-data.ts @@ -4,18 +4,19 @@ import type { Resource } from '@angular/core'; * Four mutually exclusive async states — the data lives ON the state, so e.g. * "success with no value" or "error with a stale value" is unrepresentable. */ -export type RemoteData = +export type RemoteData = | { tag: 'Loading' } | { tag: 'Empty' } - | { tag: 'Failure'; error: E } + | { tag: 'Failure'; error: Error } | { tag: 'Success'; value: T }; /** Project Angular's own resource() into a RemoteData value. */ export function fromResource( r: Resource, isEmpty: (v: T) => boolean = () => false, -): RemoteData { - if (r.status() === 'error') return { tag: 'Failure', error: r.error() }; +): RemoteData { + const error = r.error(); + if (error) return { tag: 'Failure', error }; if (r.hasValue()) { const v = r.value(); return isEmpty(v) ? { tag: 'Empty' } : { tag: 'Success', value: v }; diff --git a/src/app/shared/infrastructure/http.ts b/src/app/shared/infrastructure/http.ts new file mode 100644 index 0000000..c720e6a --- /dev/null +++ b/src/app/shared/infrastructure/http.ts @@ -0,0 +1,34 @@ +export const API_BASE_URL = 'https://jsonplaceholder.typicode.com'; + +/** Server answered, but not with a 2xx. */ +export class HttpError extends Error { + constructor( + readonly status: number, + path: string, + ) { + super(`HTTP ${status} for ${path}`); + this.name = 'HttpError'; + } +} + +/** Server answered 2xx, but the body isn't the shape we asked for. */ +export class ParseError extends Error { + constructor(what: string) { + super(`Malformed response: expected ${what}`); + this.name = 'ParseError'; + } +} + +/** + * GET + status check + parse, in that order. Throws HttpError or ParseError — + * resource() turns a thrown error into RemoteData's Failure branch. + */ +export async function getJson( + path: string, + parse: (value: unknown) => T, + abortSignal?: AbortSignal, +): Promise { + const response = await fetch(`${API_BASE_URL}${path}`, { signal: abortSignal }); + if (!response.ok) throw new HttpError(response.status, path); + return parse(await response.json()); +} diff --git a/src/app/shared/ui/molecules/async.component.spec.ts b/src/app/shared/ui/molecules/async.component.spec.ts index 4a4292f..67a45b6 100644 --- a/src/app/shared/ui/molecules/async.component.spec.ts +++ b/src/app/shared/ui/molecules/async.component.spec.ts @@ -13,11 +13,11 @@ import type { RemoteData } from '@shared/application/remote-data'; `, }) class HostComponent { - data: RemoteData = { tag: 'Loading' }; + data: RemoteData = { tag: 'Loading' }; retried = false; } -function render(data: RemoteData) { +function render(data: RemoteData) { const fixture = TestBed.createComponent(HostComponent); fixture.componentInstance.data = data; fixture.detectChanges(); diff --git a/src/app/shared/ui/molecules/async.component.ts b/src/app/shared/ui/molecules/async.component.ts index 57a01b8..7211656 100644 --- a/src/app/shared/ui/molecules/async.component.ts +++ b/src/app/shared/ui/molecules/async.component.ts @@ -24,7 +24,7 @@ import type { RemoteData } from '@shared/application/remote-data'; `, }) export class AsyncComponent { - data = input.required>(); + data = input.required>(); emptyText = input('No data.'); errorText = input('Something went wrong.'); retryText = input('Retry'); diff --git a/src/app/users/application/user-detail.resource.ts b/src/app/users/application/user-detail.resource.ts index 00e90e7..0dc995a 100644 --- a/src/app/users/application/user-detail.resource.ts +++ b/src/app/users/application/user-detail.resource.ts @@ -2,4 +2,7 @@ import { resource } from '@angular/core'; import { fetchUserById } from '@users/infrastructure/users.adapter'; export const userDetailResource = (userId: () => number) => - resource({ params: userId, loader: ({ params }) => fetchUserById(params) }); + resource({ + params: userId, + loader: ({ params, abortSignal }) => fetchUserById(params, abortSignal), + }); diff --git a/src/app/users/application/users.resource.ts b/src/app/users/application/users.resource.ts index 69e1152..c5a8b28 100644 --- a/src/app/users/application/users.resource.ts +++ b/src/app/users/application/users.resource.ts @@ -1,4 +1,5 @@ import { resource } from '@angular/core'; import { fetchUsers } from '@users/infrastructure/users.adapter'; -export const usersResource = () => resource({ loader: fetchUsers }); +export const usersResource = () => + resource({ loader: ({ abortSignal }) => fetchUsers(abortSignal) }); diff --git a/src/app/users/infrastructure/users.adapter.spec.ts b/src/app/users/infrastructure/users.adapter.spec.ts new file mode 100644 index 0000000..6e83c39 --- /dev/null +++ b/src/app/users/infrastructure/users.adapter.spec.ts @@ -0,0 +1,71 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { HttpError, ParseError } from '@shared/infrastructure/http'; +import { fetchUserById, fetchUsers, parseUser, parseUserDetail, parseUsers } from './users.adapter'; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe('fetchUsers', () => { + it('resolves with the parsed list on a 200 response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve([{ id: 1, name: 'Ada' }]), + }), + ); + await expect(fetchUsers()).resolves.toEqual([{ id: 1, name: 'Ada' }]); + }); + + it('rejects with HttpError when the server answers with a non-2xx status', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 404 })); + await expect(fetchUsers()).rejects.toBeInstanceOf(HttpError); + }); +}); + +describe('fetchUserById', () => { + it('resolves with the parsed user on a 200 response', async () => { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ id: 1, name: 'Ada', email: 'ada@example.com' }), + }), + ); + await expect(fetchUserById(1)).resolves.toEqual({ + id: 1, + name: 'Ada', + email: 'ada@example.com', + }); + }); + + it('rejects with HttpError when the server answers with a non-2xx status', async () => { + vi.stubGlobal('fetch', vi.fn().mockResolvedValue({ ok: false, status: 500 })); + await expect(fetchUserById(1)).rejects.toBeInstanceOf(HttpError); + }); +}); + +describe('parseUsers', () => { + it('throws ParseError when the response is not an array', () => { + expect(() => parseUsers({})).toThrow(ParseError); + }); + + it('throws ParseError when a list item is missing a name', () => { + expect(() => parseUsers([{ id: 1 }])).toThrow(ParseError); + }); +}); + +describe('parseUser', () => { + it('throws ParseError when id is not a number', () => { + expect(() => parseUser({ id: '1', name: 'Ada' })).toThrow(ParseError); + }); +}); + +describe('parseUserDetail', () => { + it('throws ParseError when email is missing', () => { + expect(() => parseUserDetail({ id: 1, name: 'Ada' })).toThrow(ParseError); + }); +}); diff --git a/src/app/users/infrastructure/users.adapter.ts b/src/app/users/infrastructure/users.adapter.ts index 806a2c6..af6297d 100644 --- a/src/app/users/infrastructure/users.adapter.ts +++ b/src/app/users/infrastructure/users.adapter.ts @@ -1,7 +1,32 @@ +import { getJson, ParseError } from '@shared/infrastructure/http'; import type { User, UserDetail } from '@users/domain/user'; -export const fetchUsers = (): Promise => - fetch('https://jsonplaceholder.typicode.com/users').then((r) => r.json()); +const isRecord = (value: unknown): value is Record => + typeof value === 'object' && value !== null; -export const fetchUserById = (id: number): Promise => - fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then((r) => r.json()); +export function parseUser(value: unknown): User { + if (!isRecord(value)) throw new ParseError('a user'); + const { id, name } = value; + if (typeof id !== 'number' || typeof name !== 'string') throw new ParseError('a user'); + return { id, name }; +} + +export function parseUserDetail(value: unknown): UserDetail { + if (!isRecord(value)) throw new ParseError('a user detail'); + const { id, name, email } = value; + if (typeof id !== 'number' || typeof name !== 'string' || typeof email !== 'string') { + throw new ParseError('a user detail'); + } + return { id, name, email }; +} + +export function parseUsers(value: unknown): User[] { + if (!Array.isArray(value)) throw new ParseError('a user list'); + return value.map(parseUser); +} + +export const fetchUsers = (abortSignal?: AbortSignal): Promise => + getJson('/users', parseUsers, abortSignal); + +export const fetchUserById = (id: number, abortSignal?: AbortSignal): Promise => + getJson(`/users/${id}`, parseUserDetail, abortSignal); diff --git a/src/app/users/ui/organisms/user-detail.component.spec.ts b/src/app/users/ui/organisms/user-detail.component.spec.ts index f73f4f2..a3a6273 100644 --- a/src/app/users/ui/organisms/user-detail.component.spec.ts +++ b/src/app/users/ui/organisms/user-detail.component.spec.ts @@ -11,6 +11,8 @@ describe('UserDetailComponent', () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ + ok: true, + status: 200, json: () => Promise.resolve({ id: 1, name: 'Ada', email: 'ada@example.com' }), }), ); @@ -27,6 +29,8 @@ describe('UserDetailComponent', () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue({ + ok: true, + status: 200, json: () => Promise.resolve({ id: 1, name: 'Ada', email: 'a@x.com' }), }), ); diff --git a/src/app/users/ui/organisms/user-detail.component.ts b/src/app/users/ui/organisms/user-detail.component.ts index c3fbbc0..f47db10 100644 --- a/src/app/users/ui/organisms/user-detail.component.ts +++ b/src/app/users/ui/organisms/user-detail.component.ts @@ -7,9 +7,10 @@ import { userDetailResource } from '@users/application/user-detail.resource'; selector: 'app-user-detail', imports: [AsyncComponent], template: ` - - @if (detailResource.value(); as u) { -

{{ u.name }} — {{ u.email }}

+ @let detail = data(); + + @if (detail.tag === 'Success' && detail.value) { +

{{ detail.value.name }} — {{ detail.value.email }}

}
diff --git a/src/app/users/ui/users.page.spec.ts b/src/app/users/ui/users.page.spec.ts index f02ac9f..c8c8c86 100644 --- a/src/app/users/ui/users.page.spec.ts +++ b/src/app/users/ui/users.page.spec.ts @@ -1,21 +1,55 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; import { TestBed } from '@angular/core/testing'; -import { UsersPage } from './users.page'; +import { provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness } from '@angular/router/testing'; +import { usersRoutes } from './users.routes'; function stubFetch() { vi.stubGlobal( 'fetch', vi.fn((url: string) => { if (url.endsWith('/users')) { - return Promise.resolve({ json: () => Promise.resolve([{ id: 1, name: 'Ada' }]) }); + return Promise.resolve({ + ok: true, + status: 200, + json: () => Promise.resolve([{ id: 1, name: 'Ada' }]), + }); } return Promise.resolve({ + ok: true, + status: 200, json: () => Promise.resolve({ id: 1, name: 'Ada', email: 'ada@example.com' }), }); }), ); } +function stubFetchFailure() { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ ok: false, status: 500, json: () => Promise.resolve({}) }), + ); +} + +function stubFetchEmpty() { + vi.stubGlobal( + 'fetch', + vi.fn().mockResolvedValue({ ok: true, status: 200, json: () => Promise.resolve([]) }), + ); +} + +async function renderUsersAt(url: string) { + TestBed.configureTestingModule({ + providers: [ + provideRouter([{ path: 'users', children: usersRoutes }], withComponentInputBinding()), + ], + }); + const harness = await RouterTestingHarness.create(url); + await harness.fixture.whenStable(); + harness.detectChanges(); + return harness; +} + afterEach(() => { vi.unstubAllGlobals(); }); @@ -23,44 +57,50 @@ afterEach(() => { describe('UsersPage', () => { it('renders the fetched users as a list', async () => { stubFetch(); - await TestBed.configureTestingModule({ imports: [UsersPage] }).compileComponents(); - const fixture = TestBed.createComponent(UsersPage); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('Ada'); + const harness = await renderUsersAt('/users'); + expect(harness.routeNativeElement?.textContent).toContain('Ada'); + }); + + it('shows the failure state when the request fails', async () => { + stubFetchFailure(); + const harness = await renderUsersAt('/users'); + expect(harness.routeNativeElement?.querySelector('[role="alert"]')).toBeTruthy(); + }); + + it('shows the empty state when there are no users', async () => { + stubFetchEmpty(); + const harness = await renderUsersAt('/users'); + expect(harness.routeNativeElement?.textContent).toContain('No data.'); }); it("shows a user's details after clicking their name", async () => { stubFetch(); - await TestBed.configureTestingModule({ imports: [UsersPage] }).compileComponents(); - const fixture = TestBed.createComponent(UsersPage); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - fixture.nativeElement.querySelector('button').click(); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - expect(fixture.nativeElement.textContent).toContain('ada@example.com'); + const harness = await renderUsersAt('/users'); + harness.routeNativeElement!.querySelector('button')!.click(); + await harness.fixture.whenStable(); + harness.detectChanges(); + expect(harness.routeNativeElement?.textContent).toContain('ada@example.com'); }); it('returns to the list when the detail view is closed', async () => { stubFetch(); - await TestBed.configureTestingModule({ imports: [UsersPage] }).compileComponents(); - const fixture = TestBed.createComponent(UsersPage); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - fixture.nativeElement.querySelector('button').click(); - fixture.detectChanges(); - await fixture.whenStable(); - fixture.detectChanges(); - const backButton = Array.from(fixture.nativeElement.querySelectorAll('button')).find( + const harness = await renderUsersAt('/users'); + harness.routeNativeElement!.querySelector('button')!.click(); + await harness.fixture.whenStable(); + harness.detectChanges(); + const backButton = Array.from(harness.routeNativeElement!.querySelectorAll('button')).find( (b) => (b as HTMLElement).textContent === 'Back', ) as HTMLElement; backButton.click(); - fixture.detectChanges(); - expect(fixture.nativeElement.querySelector('app-user-detail')).toBeFalsy(); + await harness.fixture.whenStable(); + harness.detectChanges(); + expect(harness.routeNativeElement?.querySelector('app-user-detail')).toBeFalsy(); + expect(harness.routeNativeElement?.textContent).toContain('Ada'); + }); + + it('deep-links directly to a user detail view', async () => { + stubFetch(); + const harness = await renderUsersAt('/users/1'); + expect(harness.routeNativeElement?.textContent).toContain('ada@example.com'); }); }); diff --git a/src/app/users/ui/users.page.ts b/src/app/users/ui/users.page.ts index 5a2de52..c627cf1 100644 --- a/src/app/users/ui/users.page.ts +++ b/src/app/users/ui/users.page.ts @@ -1,4 +1,5 @@ -import { Component, computed, signal } from '@angular/core'; +import { Component, computed, inject, input } from '@angular/core'; +import { Router } from '@angular/router'; import { PageShellComponent } from '@shared/ui/templates/page-shell.component'; import { AsyncComponent } from '@shared/ui/molecules/async.component'; import { fromResource } from '@shared/application/remote-data'; @@ -13,11 +14,12 @@ import { isEmptyUserList } from '@users/domain/user'; template: ` @if (selectedUserId(); as id) { - + } @else { - - @if (usersResource.hasValue()) { - + @let list = listData(); + + @if (list.tag === 'Success' && list.value) { + } } @@ -25,7 +27,12 @@ import { isEmptyUserList } from '@users/domain/user'; `, }) export class UsersPage { - protected selectedUserId = signal(null); + protected router = inject(Router); + userId = input(); + protected selectedUserId = computed(() => { + const id = this.userId(); + return id ? Number(id) : null; + }); protected usersResource = usersResource(); protected listData = computed(() => fromResource(this.usersResource, isEmptyUserList)); } diff --git a/src/app/users/ui/users.routes.ts b/src/app/users/ui/users.routes.ts new file mode 100644 index 0000000..3bc9f3a --- /dev/null +++ b/src/app/users/ui/users.routes.ts @@ -0,0 +1,7 @@ +import { Routes } from '@angular/router'; +import { UsersPage } from './users.page'; + +export const usersRoutes: Routes = [ + { path: '', component: UsersPage }, + { path: ':userId', component: UsersPage }, +]; diff --git a/src/index.html b/src/index.html index 3aa7e70..69531f2 100644 --- a/src/index.html +++ b/src/index.html @@ -1,13 +1,13 @@ - - - NgSignalsTemplate - - - - - - - + + + NgSignalsTemplate + + + + + + + diff --git a/tsconfig.app.json b/tsconfig.app.json index cb151e1..1eb42f4 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -5,10 +5,6 @@ "compilerOptions": { "types": [] }, - "include": [ - "src/**/*.ts" - ], - "exclude": [ - "src/**/*.spec.ts" - ] + "include": ["src/**/*.ts"], + "exclude": ["src/**/*.spec.ts"] } diff --git a/tsconfig.json b/tsconfig.json index 4b42e3f..abed8ff 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ { "compileOnSave": false, "compilerOptions": { + "strict": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noImplicitReturns": true, @@ -21,6 +22,7 @@ } }, "angularCompilerOptions": { + "strictTemplates": true, "enableI18nLegacyMessageIdFormat": false, "strictInjectionParameters": true, "strictInputAccessModifiers": true diff --git a/tsconfig.spec.json b/tsconfig.spec.json index 9c8efb9..aecce35 100644 --- a/tsconfig.spec.json +++ b/tsconfig.spec.json @@ -3,12 +3,7 @@ { "extends": "./tsconfig.json", "compilerOptions": { - "types": [ - "vitest/globals" - ] + "types": ["vitest/globals"] }, - "include": [ - "src/**/*.d.ts", - "src/**/*.spec.ts" - ] + "include": ["src/**/*.d.ts", "src/**/*.spec.ts"] }