Skip to content

Commit 9157e28

Browse files
Bump to TypeScript 5.7 with strictFunctionTypes
This commit migrates from TypeScript 5.6 to 5.7 with `strictFunctionTypes` flag enabled for stricter quality controls. Other changes: - Create type helpers for `Object.keys` and `Object.entries` to explicitly document their usage and intent. - Update `vue-tsc` to latest to prevent build errors with TypeScript 5.7.x.
1 parent 5f33dd5 commit 9157e28

File tree

38 files changed

+242
-170
lines changed

38 files changed

+242
-170
lines changed

package-lock.json

Lines changed: 8 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
"start-server-and-test": "^2.0.10",
8080
"terser": "^5.39.0",
8181
"tslib": "^2.8.1",
82-
"typescript": "~5.6.3",
82+
"typescript": "~5.7.3",
8383
"vite": "^6.2.0",
8484
"vite-plugin-minify": "^2.1.0",
8585
"vitest": "^3.0.7",

src/TypeHelpers.ts

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,19 @@
11
export type Constructible<T, TArgs extends unknown[] = never> = {
22
prototype: T;
3-
apply: (this: unknown, args: TArgs) => void;
3+
apply: (this: () => unknown, args: TArgs) => void;
44
readonly name: string;
55
};
66

77
export type PropertyKeys<T> = {
8-
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? never : K;
8+
[K in keyof T]: T[K] extends (...args: never[]) => unknown ? never : K
99
}[keyof T];
1010

11+
1112
export type ConstructorArguments<T> =
1213
T extends new (...args: infer U) => unknown ? U : never;
1314

1415
export type FunctionKeys<T> = {
15-
[K in keyof T]: T[K] extends (...args: unknown[]) => unknown ? K : never;
16+
[K in keyof T]: T[K] extends (...args: never[]) => unknown ? K : never;
1617
}[keyof T];
1718

1819
export function isString(value: unknown): value is string {
@@ -46,3 +47,30 @@ export function isPlainObject(
4647
export function isNullOrUndefined(value: unknown): value is (null | undefined) {
4748
return typeof value === 'undefined' || value === null;
4849
}
50+
51+
/**
52+
* Gets keys of object T, assuming all properties match the type.
53+
* Warning: May include properties not defined in T's type.
54+
* Details: https://stackoverflow.com/questions/55012174/why-doesnt-object-keys-return-a-keyof-type-in-typescript
55+
*/
56+
export function getUnsafeTypedKeys<T extends object>(obj: T): (keyof T)[] {
57+
return downcast(Object.keys(obj));
58+
}
59+
60+
/**
61+
* Gets entries of object T, assuming all properties match the type.
62+
* Warning: May include properties not defined in T's type.
63+
* Details: https://stackoverflow.com/questions/60141960/typescript-key-value-relation-preserving-object-entries-type
64+
*/
65+
export function getUnsafeTypedEntries<T extends object>(
66+
obj: T,
67+
): TypedEntries<T> {
68+
return downcast(Object.entries(obj));
69+
}
70+
type TypedEntries<T> = readonly ({
71+
[K in keyof T]: [K, T[K]];
72+
}[keyof T])[];
73+
74+
function downcast<T = object>(obj: object): T {
75+
return obj as T;
76+
}

src/application/Common/Timing/Throttle.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import { PlatformTimer } from './PlatformTimer';
33
import type { Timer, TimeoutType } from './Timer';
44

5-
export type CallbackType = (..._: readonly unknown[]) => void;
5+
export type CallbackType<TCallbackArgs extends unknown[]> = (
6+
..._: TCallbackArgs
7+
) => void;
68

79
export interface ThrottleOptions {
810
/** Skip the immediate execution of the callback on the first invoke */
@@ -16,42 +18,42 @@ const DefaultOptions: ThrottleOptions = {
1618
};
1719

1820
export interface ThrottleFunction {
19-
(
20-
callback: CallbackType,
21+
<TCallbackArgs extends unknown[]>(
22+
callback: CallbackType<TCallbackArgs>,
2123
waitInMs: number,
2224
options?: Partial<ThrottleOptions>,
23-
): CallbackType;
25+
): CallbackType<TCallbackArgs>;
2426
}
2527

26-
export const throttle: ThrottleFunction = (
27-
callback: CallbackType,
28+
export const throttle: ThrottleFunction = <TCallbackArgs extends unknown[]>(
29+
callback: CallbackType<TCallbackArgs>,
2830
waitInMs: number,
2931
options: Partial<ThrottleOptions> = DefaultOptions,
30-
): CallbackType => {
32+
): CallbackType<TCallbackArgs> => {
3133
const defaultedOptions: ThrottleOptions = {
3234
...DefaultOptions,
3335
...options,
3436
};
3537
const throttler = new Throttler(waitInMs, callback, defaultedOptions);
36-
return (...args: unknown[]) => throttler.invoke(...args);
38+
return (...args: TCallbackArgs) => throttler.invoke(...args);
3739
};
3840

39-
class Throttler {
41+
class Throttler<TCallbackArgs extends unknown[]> {
4042
private lastExecutionTime: number | null = null;
4143

4244
private executionScheduler: DelayedCallbackScheduler;
4345

4446
constructor(
4547
private readonly waitInMs: number,
46-
private readonly callback: CallbackType,
48+
private readonly callback: CallbackType<TCallbackArgs>,
4749
private readonly options: ThrottleOptions,
4850
) {
4951
if (!waitInMs) { throw new Error('missing delay'); }
5052
if (waitInMs < 0) { throw new Error('negative delay'); }
5153
this.executionScheduler = new DelayedCallbackScheduler(options.timer);
5254
}
5355

54-
public invoke(...args: unknown[]): void {
56+
public invoke(...args: TCallbackArgs): void {
5557
switch (true) {
5658
case this.isLeadingCallWithinThrottlePeriod(): {
5759
if (this.options.excludeLeadingCall) {
@@ -92,7 +94,7 @@ class Throttler {
9294
return this.executionScheduler.getNext() !== null;
9395
}
9496

95-
private scheduleNext(args: unknown[]): void {
97+
private scheduleNext(args: TCallbackArgs): void {
9698
if (this.executionScheduler.getNext()) {
9799
throw new Error('An execution is already scheduled.');
98100
}
@@ -102,7 +104,7 @@ class Throttler {
102104
);
103105
}
104106

105-
private updateNextScheduled(args: unknown[]): void {
107+
private updateNextScheduled(args: TCallbackArgs): void {
106108
const nextScheduled = this.executionScheduler.getNext();
107109
if (!nextScheduled) {
108110
throw new Error('A non-existent scheduled execution cannot be updated.');
@@ -114,7 +116,7 @@ class Throttler {
114116
);
115117
}
116118

117-
private executeNow(args: unknown[]): void {
119+
private executeNow(args: TCallbackArgs): void {
118120
this.callback(...args);
119121
this.lastExecutionTime = this.dateNow();
120122
}

src/application/Common/TypeValidator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -94,9 +94,9 @@ function assertAllowedProperties(
9494
valueName: string,
9595
allowedProperties: readonly string[],
9696
): void {
97-
const properties = Object.keys(value).map((p) => p as string);
98-
const disallowedProperties = properties.filter(
99-
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
97+
const allProperties = Object.keys(value);
98+
const disallowedProperties = allProperties.filter(
99+
(prop) => !allowedProperties.includes(prop),
100100
);
101101
if (disallowedProperties.length > 0) {
102102
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);

src/infrastructure/WindowVariables/WindowVariablesValidator.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { ContextIsolatedElectronDetector } from '@/infrastructure/RuntimeEnviron
22
import type { ElectronEnvironmentDetector } from '@/infrastructure/RuntimeEnvironment/Electron/ElectronEnvironmentDetector';
33
import { OperatingSystem } from '@/domain/OperatingSystem';
44
import {
5-
type PropertyKeys, isBoolean, isFunction, isNumber, isPlainObject,
5+
type PropertyKeys, getUnsafeTypedEntries, isBoolean, isFunction, isNumber, isPlainObject,
66
} from '@/TypeHelpers';
77
import type { WindowVariables } from './WindowVariables';
88

@@ -36,9 +36,9 @@ function* testEveryProperty(variables: Partial<WindowVariables>): Iterable<strin
3636
scriptDiagnosticsCollector: testScriptDiagnosticsCollector(variables),
3737
};
3838

39-
for (const [propertyName, testResult] of Object.entries(tests)) {
39+
for (const [propertyName, testResult] of getUnsafeTypedEntries(tests)) {
4040
if (!testResult) {
41-
const propertyValue = variables[propertyName as keyof WindowVariables];
41+
const propertyValue = variables[propertyName];
4242
yield `Unexpected ${propertyName} (${typeof propertyValue})`;
4343
}
4444
}

src/presentation/components/Scripts/Slider/UseDragHandler.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import {
22
onUnmounted, ref, shallowReadonly, watch,
33
} from 'vue';
44
import { throttle } from '@/application/Common/Timing/Throttle';
5+
import type { LifecycleHook } from '@/presentation/components/Shared/Hooks/Common/LifecycleHook';
56
import type { Ref } from 'vue';
6-
import type { LifecycleHook } from '../../Shared/Hooks/Common/LifecycleHook';
77

88
const ThrottleInMs = 15;
99

@@ -18,7 +18,7 @@ export function useDragHandler(
1818

1919
let initialPointerX: number | undefined;
2020

21-
const onDrag = throttler((event: PointerEvent) => {
21+
const onDrag = throttler((event: PointerEvent): void => {
2222
if (initialPointerX === undefined) {
2323
throw new Error('Resize action started without an initial X coordinate.');
2424
}
@@ -64,28 +64,34 @@ export function useDragHandler(
6464
};
6565
}
6666

67+
export type DocumentEventKey = keyof DocumentEventMap;
68+
69+
export type DocumentEventHandler<TEvent extends DocumentEventKey> = (
70+
event: DocumentEventMap[TEvent],
71+
) => void;
72+
6773
export interface DragDomModifier {
68-
addEventListenerToDocument(
69-
type: keyof DocumentEventMap,
70-
handler: EventListener,
74+
addEventListenerToDocument<TEvent extends DocumentEventKey>(
75+
type: TEvent,
76+
handler: DocumentEventHandler<TEvent>
7177
): void;
72-
removeEventListenerFromDocument(
73-
type: keyof DocumentEventMap,
74-
handler: EventListener,
78+
removeEventListenerFromDocument<TEvent extends DocumentEventKey>(
79+
type: TEvent,
80+
handler: DocumentEventHandler<TEvent>,
7581
): void;
7682
}
7783

7884
class GlobalDocumentDragDomModifier implements DragDomModifier {
79-
public addEventListenerToDocument(
80-
type: keyof DocumentEventMap,
81-
listener: EventListener,
85+
public addEventListenerToDocument<TEvent extends DocumentEventKey>(
86+
type: TEvent,
87+
listener: DocumentEventHandler<TEvent>,
8288
): void {
8389
document.addEventListener(type, listener);
8490
}
8591

86-
public removeEventListenerFromDocument(
87-
type: keyof DocumentEventMap,
88-
listener: EventListener,
92+
public removeEventListenerFromDocument<TEvent extends DocumentEventKey>(
93+
type: TEvent,
94+
listener: DocumentEventHandler<TEvent>,
8995
): void {
9096
document.removeEventListener(type, listener);
9197
}

src/presentation/components/Scripts/View/Tree/TreeViewAdapter/UseTreeViewFilterEvent.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import {
22
type Ref, shallowReadonly, shallowRef,
33
} from 'vue';
4-
import type { Category } from '@/domain/Executables/Category/Category';
54
import { injectKey } from '@/presentation/injectionSymbols';
65
import type { ReadonlyFilterContext } from '@/application/Context/State/Filter/FilterContext';
76
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
@@ -81,6 +80,6 @@ function containsExecutable(
8180
executables: readonly Executable[],
8281
): boolean {
8382
return executables.some(
84-
(existing: Category) => existing.executableId === expectedId,
83+
(existing) => existing.executableId === expectedId,
8584
);
8685
}

src/presentation/components/Shared/ExpandCollapse/UseExpandCollapseAnimation.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { PlatformTimer } from '@/application/Common/Timing/PlatformTimer';
22
import type { Timer } from '@/application/Common/Timing/Timer';
3+
import { getUnsafeTypedEntries } from '@/TypeHelpers';
34

45
export type AnimationFunction = (element: Element) => Promise<void>;
56

@@ -98,8 +99,8 @@ function setTransitionPropertiesToElementDimensions(
9899
elementStyle: ElementStyleMutator,
99100
elementDimensions: TransitionStyleRecords,
100101
): void {
101-
Object.entries(elementDimensions).forEach(([key, value]) => {
102-
elementStyle.changeStyle(key as AnimatedStyleProperty, value);
102+
getUnsafeTypedEntries(elementDimensions).forEach(([key, value]) => {
103+
elementStyle.changeStyle(key, value);
103104
});
104105
}
105106

@@ -175,8 +176,8 @@ function restoreOriginalStyles(
175176
element: HTMLElement,
176177
originalStyles: MutatedStyleProperties,
177178
): void {
178-
Object.entries(originalStyles).forEach(([key, value]) => {
179-
element.style[key as MutatedStyleProperty] = value;
179+
getUnsafeTypedEntries(originalStyles).forEach(([key, value]) => {
180+
element.style[key] = value;
180181
});
181182
}
182183

src/presentation/components/Shared/Hooks/UseAutoUnsubscribedEventListener.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,14 @@ export interface TargetEventListener {
4040
startListening<TEvent extends keyof HTMLElementEventMap>(
4141
eventTargetSource: EventTargetOrRef,
4242
eventType: TEvent,
43-
eventHandler: (event: HTMLElementEventMap[TEvent]) => void,
43+
eventHandler: TypedEventHandler<TEvent>,
4444
): void;
4545
}
4646

4747
function startListeningRef<TEvent extends keyof HTMLElementEventMap>(
4848
eventTargetRef: Readonly<Ref<EventTarget | undefined>>,
4949
eventType: TEvent,
50-
eventHandler: (event: HTMLElementEventMap[TEvent]) => void,
50+
eventHandler: TypedEventHandler<TEvent>,
5151
onTeardown: LifecycleHook,
5252
): void {
5353
const eventListenerManager = new EventListenerManager();
@@ -78,9 +78,14 @@ class EventListenerManager {
7878
public addListener<TEvent extends keyof HTMLElementEventMap>(
7979
eventTarget: EventTarget,
8080
eventType: TEvent,
81-
eventHandler: (event: HTMLElementEventMap[TEvent]) => void,
81+
eventHandler: TypedEventHandler<TEvent>,
8282
) {
83-
eventTarget.addEventListener(eventType, eventHandler);
84-
this.removeListener = () => eventTarget.removeEventListener(eventType, eventHandler);
83+
const listener = eventHandler as EventListener;
84+
eventTarget.addEventListener(eventType, listener);
85+
this.removeListener = () => eventTarget.removeEventListener(eventType, listener);
8586
}
8687
}
88+
89+
type TypedEventHandler<
90+
TEvent extends (keyof HTMLElementEventMap),
91+
> = ((event: HTMLElementEventMap[TEvent]) => void);

0 commit comments

Comments
 (0)