refactor: improve typing

This commit is contained in:
Johannes Millan 2025-08-11 22:11:36 +02:00
parent 5bfa9539f1
commit 632c140894
39 changed files with 181 additions and 93 deletions

View file

@ -15,7 +15,7 @@ import { debounce } from '../../util/decorators';
providedIn: 'root',
})
export class SnackService {
private _store$ = inject<Store<any>>(Store);
private _store$ = inject(Store);
private _translateService = inject(TranslateService);
private _actions$ = inject(Actions);
private _matSnackBar = inject(MatSnackBar);
@ -80,7 +80,7 @@ export class SnackService {
if (showWhile$ || promise || isSpinner) {
// TODO check if still needed
(cfg as any).panelClass = 'polling-snack';
(cfg as { panelClass: string }).panelClass = 'polling-snack';
}
switch (type) {

View file

@ -39,7 +39,6 @@ import { clockStringFromDate } from '../../../ui/duration/clock-string-from-date
import { HelpSectionComponent } from '../../../ui/help-section/help-section.component';
import { ChipListInputComponent } from '../../../ui/chip-list-input/chip-list-input.component';
import { MatButton } from '@angular/material/button';
import { AsyncPipe } from '@angular/common';
import { MatIcon } from '@angular/material/icon';
import { Log } from '../../../core/log';
import { toSignal } from '@angular/core/rxjs-interop';
@ -59,7 +58,6 @@ import { toSignal } from '@angular/core/rxjs-interop';
ChipListInputComponent,
MatDialogActions,
MatButton,
AsyncPipe,
MatIcon,
],
})

View file

@ -38,7 +38,6 @@ import { WorkContextService } from '../work-context/work-context.service';
import { ProjectService } from '../project/project.service';
import { TaskViewCustomizerService } from '../task-view-customizer/task-view-customizer.service';
import { toSignal } from '@angular/core/rxjs-interop';
import { RightPanelComponent } from '../right-panel/right-panel.component';
import { CdkDropListGroup } from '@angular/cdk/drag-drop';
import { CdkScrollable } from '@angular/cdk/scrolling';
import { MatTooltip } from '@angular/material/tooltip';
@ -77,7 +76,6 @@ import { FinishDayBtnComponent } from './finish-day-btn/finish-day-btn.component
],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
RightPanelComponent,
CdkDropListGroup,
CdkScrollable,
MatTooltip,

View file

@ -57,7 +57,6 @@ import { MsToClockStringPipe } from '../../ui/duration/ms-to-clock-string.pipe';
import { TranslatePipe } from '@ngx-translate/core';
import { TaskSummaryTablesComponent } from '../../features/tasks/task-summary-tables/task-summary-tables.component';
import { TasksByTagComponent } from '../../features/tasks/tasks-by-tag/tasks-by-tag.component';
import { RightPanelComponent } from '../../features/right-panel/right-panel.component';
import { EvaluationSheetComponent } from '../../features/metric/evaluation-sheet/evaluation-sheet.component';
import { WorklogWeekComponent } from '../../features/worklog/worklog-week/worklog-week.component';
import { InlineMarkdownComponent } from '../../ui/inline-markdown/inline-markdown.component';
@ -99,7 +98,6 @@ const MAGIC_YESTERDAY_MARGIN = 4 * 60 * 60 * 1000;
TranslatePipe,
TaskSummaryTablesComponent,
TasksByTagComponent,
RightPanelComponent,
EvaluationSheetComponent,
WorklogWeekComponent,
InlineMarkdownComponent,

View file

@ -40,7 +40,7 @@ interface Suggestion {
id: string;
title: string;
[key: string]: any;
[key: string]: unknown;
}
@Component({

View file

@ -32,7 +32,7 @@ export class DialogConfirmComponent {
readonly T: typeof T = T;
close(res: any): void {
close(res: boolean | string | undefined): void {
this._matDialogRef.close(res);
}

View file

@ -4,11 +4,17 @@ import { SimpleDuration } from '../../util/round-duration';
@Pipe({ name: 'durationFromString' })
export class DurationFromStringPipe implements PipeTransform {
transform: (value: any, ...args: any[]) => any = durationFromString;
transform: (
value: string | null | undefined,
...args: unknown[]
) => SimpleDuration | null = durationFromString;
}
export const durationFromString = (strValue: any, args?: any): SimpleDuration | null => {
const milliseconds = stringToMs(strValue);
export const durationFromString = (
strValue: string | null | undefined,
args?: unknown,
): SimpleDuration | null => {
const milliseconds = stringToMs(strValue || '');
if (milliseconds > 0) {
return {
asMilliseconds: () => milliseconds,

View file

@ -1,12 +1,29 @@
import { Pipe, PipeTransform } from '@angular/core';
type DurationInput =
| number
| string
| { asMilliseconds(): number }
| { _milliseconds: number }
| {
_data: {
milliseconds?: number;
seconds?: number;
minutes?: number;
hours?: number;
days?: number;
};
}
| null
| undefined;
@Pipe({ name: 'durationToString' })
export class DurationToStringPipe implements PipeTransform {
transform: (value: any, ...args: any[]) => any = durationToString;
transform: (value: DurationInput, ...args: unknown[]) => string = durationToString;
}
/* eslint-disable no-mixed-operators */
export const durationToString = (value: any, args?: any): any => {
export const durationToString = (value: DurationInput, args?: unknown): string => {
if (!value) {
return '';
}
@ -18,16 +35,31 @@ export const durationToString = (value: any, args?: any): any => {
milliseconds = value;
}
// Handle SimpleDuration object with asMilliseconds method
else if (value.asMilliseconds && typeof value.asMilliseconds === 'function') {
else if (
typeof value === 'object' &&
value !== null &&
'asMilliseconds' in value &&
typeof value.asMilliseconds === 'function'
) {
milliseconds = value.asMilliseconds();
}
// Handle object with _milliseconds property (moment-like)
else if (value._milliseconds) {
milliseconds = value._milliseconds;
else if (typeof value === 'object' && value !== null && '_milliseconds' in value) {
milliseconds = (value as { _milliseconds: number })._milliseconds;
}
// Handle object with _data property (moment duration internal structure)
else if (value._data) {
const dd = value._data;
else if (typeof value === 'object' && value !== null && '_data' in value) {
const dd = (
value as {
_data: {
milliseconds?: number;
seconds?: number;
minutes?: number;
hours?: number;
days?: number;
};
}
)._data;
milliseconds =
(dd.milliseconds || 0) +
(dd.seconds || 0) * 1000 +

View file

@ -73,7 +73,7 @@ describe('InputDurationDirective', () => {
return 0;
});
mockMsToStringPipe.transform.and.callFake((ms: number) => {
mockMsToStringPipe.transform.and.callFake((ms: number | null | undefined) => {
if (ms === 7380000) return '2h 3m';
if (ms === 7200000) return '2h';
if (ms === 3600000) return '1h';
@ -200,7 +200,7 @@ describe('InputDurationDirective', () => {
return 0;
});
mockMsToStringPipe.transform.and.callFake((ms: number) => {
mockMsToStringPipe.transform.and.callFake((ms: number | null | undefined) => {
if (ms === 3600000) return '1h';
if (ms === 1800000) return '30m';
if (ms === 5400000) return '1h 30m';

View file

@ -88,11 +88,11 @@ export class InputDurationDirective implements ControlValueAccessor, Validator,
this._formatDisplayValue();
}
registerOnChange(fn: any): void {
registerOnChange(fn: (value: number | null) => void): void {
this._onChange = fn;
}
registerOnTouched(fn: any): void {
registerOnTouched(fn: () => void): void {
this._onTouched = fn;
}

View file

@ -1,7 +1,7 @@
import { Pipe, PipeTransform } from '@angular/core';
export const msToClockString = (
value: any,
value: number | null | undefined,
showSeconds?: boolean,
isHideEmptyPlaceholder?: boolean,
): string => {
@ -32,5 +32,6 @@ export const msToClockString = (
@Pipe({ name: 'msToClockString' })
export class MsToClockStringPipe implements PipeTransform {
transform: (value: any, ...args: any[]) => any = msToClockString;
transform: (value: number | null | undefined, ...args: [boolean?, boolean?]) => string =
msToClockString;
}

View file

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
export const msToMinuteClockString = (value: any): string => {
export const msToMinuteClockString = (value: number | null | undefined): string => {
const totalMs = Number(value) || 0;
const totalSeconds = Math.floor(Math.abs(totalMs) / 1000);
const totalMinutes = Math.floor(totalSeconds / 60);
@ -15,5 +15,6 @@ export const msToMinuteClockString = (value: any): string => {
@Pipe({ name: 'msToMinuteClockString' })
export class MsToMinuteClockStringPipe implements PipeTransform {
transform: (value: any, ...args: any[]) => any = msToMinuteClockString;
transform: (value: number | null | undefined, ...args: unknown[]) => string =
msToMinuteClockString;
}

View file

@ -5,15 +5,16 @@ const M = S * 60;
const H = M * 60;
export const msToString = (
value: any,
value: number | null | undefined,
isShowSeconds?: boolean,
isHideEmptyPlaceholder?: boolean,
): string => {
const hours = Math.floor(value / H);
const numValue = Number(value) || 0;
const hours = Math.floor(numValue / H);
// prettier-ignore
const minutes = Math.floor((value - (hours * H)) / M);
const minutes = Math.floor((numValue - (hours * H)) / M);
// prettier-ignore
const seconds = isShowSeconds ? Math.floor((value - (hours * H) - (minutes * M)) / S) : 0;
const seconds = isShowSeconds ? Math.floor((numValue - (hours * H) - (minutes * M)) / S) : 0;
const parsed =
// ((+md.days() > 0) ? (md.days() + 'd ') : '')
@ -31,7 +32,7 @@ export const msToString = (
@Pipe({ name: 'msToString' })
export class MsToStringPipe implements PipeTransform {
transform: (
value: any,
value: number | null | undefined,
isShowSeconds?: boolean,
isHideEmptyPlaceholder?: boolean,
) => string = msToString;

View file

@ -1,6 +1,6 @@
import { Pipe, PipeTransform } from '@angular/core';
export const stringToMs = (strValue: string, args?: any): number => {
export const stringToMs = (strValue: string, args?: unknown): number => {
if (!strValue) {
return 0;
}
@ -87,5 +87,5 @@ export const stringToMs = (strValue: string, args?: any): number => {
@Pipe({ name: 'stringToMs' })
export class StringToMsPipe implements PipeTransform {
transform: (value: any, ...args: any[]) => any = stringToMs;
transform: (value: string, ...args: unknown[]) => number = stringToMs;
}

View file

@ -6,7 +6,7 @@ import { humanizeTimestamp } from '../../util/humanize-timestamp';
export class HumanizeTimestampPipe implements PipeTransform {
private translateService = inject(TranslateService);
transform(value: any): any {
transform(value: number | Date | string): string {
return humanizeTimestamp(value, this.translateService);
}
}

View file

@ -5,8 +5,12 @@ import { Pipe, PipeTransform } from '@angular/core';
pure: false,
})
export class KeysPipe implements PipeTransform {
transform(value: any, sort: any, filterOutKeys?: any): any {
if (value === Object(value)) {
transform(
value: Record<string, unknown> | null | undefined,
sort: boolean | 'reverse',
filterOutKeys?: string | string[],
): string[] | null {
if (value && value === Object(value)) {
const keys = Object.keys(value);
if (typeof filterOutKeys === 'string') {
@ -14,6 +18,13 @@ export class KeysPipe implements PipeTransform {
if (index > -1) {
keys.splice(index, 1);
}
} else if (Array.isArray(filterOutKeys)) {
filterOutKeys.forEach((key) => {
const index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
}
});
}
if (sort) {

View file

@ -3,7 +3,10 @@ import { formatDate } from '../../util/format-date';
@Pipe({ name: 'momentFormat' })
export class MomentFormatPipe implements PipeTransform {
transform(value: any, args: any): any {
transform(
value: number | Date | string | null | undefined,
args: string | null | undefined,
): string | null {
if (value && args) {
const result = formatDate(value, args);
return result || null;

View file

@ -17,7 +17,7 @@ const MAP = [
@Pipe({ name: 'numberToMonth' })
export class NumberToMonthPipe implements PipeTransform {
transform(value: any, args?: any): any {
return MAP[parseInt(value, 10) - 1];
transform(value: number | string, args?: unknown): string | undefined {
return MAP[parseInt(String(value), 10) - 1];
}
}

View file

@ -2,14 +2,18 @@ import { Pipe, PipeTransform } from '@angular/core';
@Pipe({ name: 'sort' })
export class SortPipe implements PipeTransform {
transform(array: any[], field: string, reverse: boolean = false): any[] {
transform<T extends Record<string, unknown>>(
array: T[],
field: keyof T,
reverse: boolean = false,
): T[] {
const f = reverse ? -1 : 1;
if (!Array.isArray(array)) {
return array;
}
const arr = [...array];
arr.sort((a: any, b: any) => {
arr.sort((a: T, b: T) => {
if (a[field] < b[field]) {
return -1 * f;
} else if (a[field] > b[field]) {

View file

@ -5,8 +5,11 @@ import { Pipe, PipeTransform } from '@angular/core';
pure: false,
})
export class ToArrayPipe implements PipeTransform {
transform(obj: any, filterOutKeys?: any): any {
if (obj === Object(obj)) {
transform(
obj: Record<string, unknown> | null | undefined,
filterOutKeys?: string | string[],
): Array<{ key: string; value: unknown }> | null {
if (obj && obj === Object(obj)) {
const keys = Object.keys(obj);
if (typeof filterOutKeys === 'string') {
@ -14,8 +17,15 @@ export class ToArrayPipe implements PipeTransform {
if (index > -1) {
keys.splice(index, 1);
}
} else if (Array.isArray(filterOutKeys)) {
filterOutKeys.forEach((key) => {
const index = keys.indexOf(key);
if (index > -1) {
keys.splice(index, 1);
}
});
}
const newArray: any[] = [];
const newArray: Array<{ key: string; value: unknown }> = [];
keys.forEach((key) => {
newArray.push({
key,

View file

@ -10,7 +10,7 @@ import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular
import { maxValidator } from './max.validator';
const MAX_VALIDATOR: any = {
const MAX_VALIDATOR = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MaxDirective),
multi: true,
@ -44,9 +44,11 @@ export class MaxDirective implements Validator, OnInit, OnChanges {
}
}
validate(c: AbstractControl): { [key: string]: any } | null {
validate(c: AbstractControl): { [key: string]: unknown } | null {
if (this._validator) {
return this._validator(c) as { [key: string]: any };
return this._validator(c) as {
[key: string]: unknown;
};
}
return null;
}

View file

@ -1,7 +1,11 @@
import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
export const maxValidator = (max: number): ValidatorFn => {
return (control: AbstractControl): { [key: string]: any } | null => {
return (
control: AbstractControl,
): {
[key: string]: unknown;
} | null => {
if (!max || Validators.required(control)) {
return null;
}

View file

@ -10,7 +10,7 @@ import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular
import { minValidator } from './min.validator';
const MIN_VALIDATOR: any = {
const MIN_VALIDATOR = {
provide: NG_VALIDATORS,
useExisting: forwardRef(() => MinDirective),
multi: true,
@ -44,9 +44,11 @@ export class MinDirective implements Validator, OnInit, OnChanges {
}
}
validate(c: AbstractControl): { [key: string]: any } | null {
validate(c: AbstractControl): { [key: string]: unknown } | null {
if (this._validator) {
return this._validator(c) as { [key: string]: any };
return this._validator(c) as {
[key: string]: unknown;
};
}
return null;
}

View file

@ -1,7 +1,11 @@
import { AbstractControl, ValidatorFn, Validators } from '@angular/forms';
export const minValidator = (min: number): ValidatorFn => {
return (control: AbstractControl): { [key: string]: any } | null => {
return (
control: AbstractControl,
): {
[key: string]: unknown;
} | null => {
if (!min || Validators.required(control)) {
return null;
}

View file

@ -6,7 +6,7 @@ const NUMBER_OF_ACTIONS_TO_SAVE = 30;
const actionLog: string[] = [];
let beforeLastErrorLog: string[] = [];
export const actionLogger = (action: any): void => {
export const actionLogger = (action: { type: string; [key: string]: unknown }): void => {
if (action.type.indexOf('@ngrx') === 0) {
return;
}

View file

@ -1,10 +1,12 @@
import { Dictionary } from '@ngrx/entity';
export const arrayToDictionary = <T>(arr: T[]): Dictionary<T> => {
export const arrayToDictionary = <T extends { id: string | number }>(
arr: T[],
): Dictionary<T> => {
return arr.reduce(
(acc: any, sc): Dictionary<unknown> => ({
(acc: Dictionary<T>, sc): Dictionary<T> => ({
...acc,
[(sc as any).id]: sc,
[sc.id]: sc,
}),
{},
);

View file

@ -1,4 +1,4 @@
export const dedupeByKey = (arr: any[], key: string): any[] => {
export const dedupeByKey = <T>(arr: T[], key: keyof T): T[] => {
const temp = arr.map((el) => el[key]);
return arr.filter((el, i) => temp.indexOf(el[key]) === i);
};

View file

@ -3,7 +3,7 @@ import { Log } from '../core/log';
let isShowAlert = true;
export const devError = (errStr: any): void => {
export const devError = (errStr: string | Error | unknown): void => {
if (environment.production) {
Log.err(errStr);
// TODO add super simple snack message if possible
@ -13,7 +13,7 @@ export const devError = (errStr: any): void => {
isShowAlert = false;
}
if (confirm(`Throw an error for error? ${errStr}`)) {
throw new Error(errStr);
throw new Error(errStr as string);
}
}
};

View file

@ -1,6 +1,6 @@
import { isObject } from './is-object';
export const distinctUntilChangedObject = (a: any, b: any): boolean => {
export const distinctUntilChangedObject = <T>(a: T, b: T): boolean => {
if ((isObject(a) && isObject(b)) || (Array.isArray(a) && Array.isArray(b))) {
return JSON.stringify(a) === JSON.stringify(b);
} else {

View file

@ -1,4 +1,4 @@
export const exists = <T>(v: any): T | never => {
export const exists = <T>(v: T | null | undefined): T | never => {
if (!v) {
throw new Error('Value is ' + v);
}

View file

@ -1,9 +1,11 @@
import { Dictionary, EntityState } from '@ngrx/entity';
import { arrayToDictionary } from './array-to-dictionary';
export const fakeEntityStateFromArray = <T>(items: Partial<T>[]): EntityState<T> => {
const dict = arrayToDictionary(items) as Dictionary<T>;
const ids = items.map((item) => (item as any).id);
export const fakeEntityStateFromArray = <T extends { id: string | number }>(
items: Partial<T>[],
): EntityState<T> => {
const dict = arrayToDictionary(items as T[]) as Dictionary<T>;
const ids = items.map((item) => (item as T).id) as string[] | number[];
return {
entities: dict,
ids,
@ -11,7 +13,7 @@ export const fakeEntityStateFromArray = <T>(items: Partial<T>[]): EntityState<T>
};
export const fakeEntityStateFromNumbersArray = <T>(...nrs: number[]): EntityState<T> => {
const items: any = nrs.map((nr) => ({ id: '_' + nr }));
const items = nrs.map((nr) => ({ id: '_' + nr }));
const dict = arrayToDictionary(items) as Dictionary<T>;
return {

View file

@ -1,19 +1,21 @@
import { isObject } from './is-object';
import { HANDLED_ERROR_PROP_STR } from '../app.constants';
export const getErrorTxt = (err: any): string => {
if (err && isObject(err.error)) {
export const getErrorTxt = (err: unknown): string => {
if (err && isObject((err as any).error)) {
return (
err.error.message ||
err.error.name ||
(err as any).error.message ||
(err as any).error.name ||
// for ngx translate...
(isObject(err.error.error) ? err.error.error.toString() : err.error) ||
err.error
(isObject((err as any).error.error)
? (err as any).error.error.toString()
: (err as any).error) ||
(err as any).error
);
} else if (err && err[HANDLED_ERROR_PROP_STR]) {
return err[HANDLED_ERROR_PROP_STR];
} else if (err && err.toString) {
return err.toString();
} else if (err && (err as any)[HANDLED_ERROR_PROP_STR]) {
return (err as any)[HANDLED_ERROR_PROP_STR];
} else if (err && (err as any).toString) {
return (err as any).toString();
} else if (typeof err === 'string') {
return err;
} else {

View file

@ -3,7 +3,7 @@ import { IS_ELECTRON } from '../app.constants';
import { devError } from './dev-error';
import { Log } from '../core/log';
const handlerMap: { [key: string]: Observable<any> } = {};
const handlerMap: { [key: string]: Observable<unknown[]> } = {};
export const ipcEvent$ = (evName: string): Observable<unknown[]> => {
if (!IS_ELECTRON) {
@ -18,7 +18,7 @@ export const ipcEvent$ = (evName: string): Observable<unknown[]> => {
}
handlerMap[evName] = subject;
const handler: (...args: any[]) => void = (...args): void => {
const handler: (...args: unknown[]) => void = (...args): void => {
Log.log('ipcEvent$ trigger', evName);
subject.next([...args]);
};

View file

@ -1 +1 @@
export const isObject = (obj: any): boolean => obj === Object(obj);
export const isObject = (obj: unknown): obj is object => obj === Object(obj);

View file

@ -6,9 +6,11 @@ export const observeWidth = (target: HTMLElement): Observable<number> => {
// eslint-disable-next-line
if ((window as any).ResizeObserver) {
// eslint-disable-next-line
const resizeObserver = new (window as any).ResizeObserver((entries: any[]) => {
observer.next(entries[0].contentRect.width);
});
const resizeObserver = new (window as any).ResizeObserver(
(entries: ResizeObserverEntry[]) => {
observer.next(entries[0].contentRect.width);
},
);
resizeObserver.observe(target);
return () => {
resizeObserver.unobserve(target);

View file

@ -1,7 +1,7 @@
export const uniqueByProp = <T>(array: T[], propName: keyof T): T[] => {
const r: T[] = [];
const allCompareKeys: string[] = [];
array.forEach((v: any) => {
const allCompareKeys: unknown[] = [];
array.forEach((v: T) => {
const compareProp = v[propName];
if (!allCompareKeys.includes(compareProp)) {
r.push(v);

View file

@ -4,14 +4,14 @@ export const updateAllInDictionary = <T>(
oldD: Dictionary<T>,
changes: Partial<T>,
): Dictionary<T> => {
const newD: any = {};
const newD: Dictionary<T> = {};
const ids = Object.keys(oldD);
ids.forEach((id: string) => {
newD[id] = {
...oldD[id],
...changes,
};
} as T;
});
return newD;

View file

@ -2,7 +2,7 @@
export const watchObject = <T extends object>(
obj: T,
onChange: (prop: string, value: any) => void,
onChange: (prop: string, value: unknown) => void,
): T =>
new Proxy(obj, {
// eslint-disable-next-line prefer-arrow/prefer-arrow-functions

View file

@ -15,25 +15,30 @@ beforeAll(() => {
// Mock browser dialogs globally for tests
// We need to handle tests that try to spy on alert/confirm after we've already mocked them
// First check if alert/confirm are already spies (from previous test runs)
if (!(window.alert as any).and) {
if (!(window.alert as jasmine.Spy).and) {
window.alert = jasmine.createSpy('alert');
}
if (!(window.confirm as any).and) {
if (!(window.confirm as jasmine.Spy).and) {
window.confirm = jasmine.createSpy('confirm').and.returnValue(true);
}
// Configure the TestBed providers globally
const originalConfigureTestingModule = TestBed.configureTestingModule;
TestBed.configureTestingModule = function (moduleDef: any) {
TestBed.configureTestingModule = function (
moduleDef: Parameters<typeof originalConfigureTestingModule>[0],
) {
if (!moduleDef.providers) {
moduleDef.providers = [];
}
// Add zoneless change detection provider if not already present
const hasZonelessProvider = moduleDef.providers.some(
(p: any) =>
(p: unknown) =>
p === provideExperimentalZonelessChangeDetection ||
(p && p.provide === provideExperimentalZonelessChangeDetection),
(p &&
typeof p === 'object' &&
'provide' in p &&
p.provide === provideExperimentalZonelessChangeDetection),
);
if (!hasZonelessProvider) {