fix: resolve TypeScript timer type errors after pnpm migration

The pnpm migration exposed type conflicts between @types/node and DOM
timer types that were previously masked by npm's flat hoisting.

Root cause: tsconfig.json had a broken type configuration:
- typeRoots pointed to non-existent "./types" directory
- @types/node was explicitly excluded

Fix:
- Replace typeRoots with explicit "types": ["node"]
- Remove @types/node from exclude list
- Update @types/node to 20.19.9 (matches Node 20.x)
- Add .npmrc with public-hoist-pattern for @types
- Use ReturnType<typeof setInterval> for server timer types
- Use window.setInterval/clearInterval for browser components

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Justin Maier
2026-01-22 17:27:10 -07:00
parent 3c8e8ab41a
commit 622770f6bd
12 changed files with 55 additions and 60 deletions
+1
View File
@@ -0,0 +1 @@
public-hoist-pattern[]=@types/*
+1 -1
View File
@@ -253,7 +253,7 @@
"@types/lodash-es": "^4.17.7",
"@types/mailchimp__mailchimp_marketing": "^3.0.12",
"@types/marked": "^4.0.7",
"@types/node": "18.11.0",
"@types/node": "20.19.9",
"@types/node-os-utils": "^1.3.1",
"@types/nodemailer": "^6.4.7",
"@types/offscreencanvas": "^2019.7.3",
+35 -40
View File
@@ -568,8 +568,8 @@ importers:
specifier: ^4.0.7
version: 4.3.2
'@types/node':
specifier: 18.11.0
version: 18.11.0
specifier: 20.19.9
version: 20.19.9
'@types/node-os-utils':
specifier: ^1.3.1
version: 1.3.4
@@ -659,7 +659,7 @@ importers:
version: 4.2.5(eslint-config-prettier@8.10.2(eslint@8.22.0))(eslint@8.22.0)(prettier@2.8.8)
eslint-plugin-tailwindcss:
specifier: ^3.15.1
version: 3.18.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2)))
version: 3.18.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2)))
husky:
specifier: ^9.1.1
version: 9.1.7
@@ -689,10 +689,10 @@ importers:
version: 1.7.0
tailwindcss:
specifier: ^3.4.3
version: 3.4.17(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2))
version: 3.4.17(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2))
ts-node:
specifier: ^10.9.1
version: 10.9.2(@types/node@18.11.0)(typescript@5.9.2)
version: 10.9.2(@types/node@20.19.9)(typescript@5.9.2)
tsx:
specifier: ^4.19.2
version: 4.20.3
@@ -2990,11 +2990,8 @@ packages:
'@types/node@16.18.126':
resolution: {integrity: sha512-OTcgaiwfGFBKacvfwuHzzn1KLxH/er8mluiy8/uM3sGXHaRe73RrSIj01jow9t4kJEW633Ov+cOexXeiApTyAw==}
'@types/node@18.11.0':
resolution: {integrity: sha512-IOXCvVRToe7e0ny7HpT/X9Rb2RYtElG1a+VshjwT00HxrM2dWBApHQoqsI6WiY7Q03vdf2bCrIGzVrkF/5t10w==}
'@types/node@18.19.121':
resolution: {integrity: sha512-bHOrbyztmyYIi4f1R0s17QsPs1uyyYnGcXeZoGEd227oZjry0q6XQBQxd82X1I57zEfwO8h9Xo+Kl5gX1d9MwQ==}
'@types/node@18.19.130':
resolution: {integrity: sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==}
'@types/node@20.19.9':
resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==}
@@ -11022,7 +11019,7 @@ snapshots:
'@types/connect@3.4.38':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/cookie@0.4.1': {}
@@ -11071,11 +11068,11 @@ snapshots:
'@types/jsonwebtoken@9.0.10':
dependencies:
'@types/ms': 2.1.0
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/keyv@3.1.4':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/linkify-it@5.0.0': {}
@@ -11106,7 +11103,7 @@ snapshots:
'@types/node-fetch@2.6.13':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
form-data: 4.0.4
'@types/node-os-utils@1.3.4': {}
@@ -11115,9 +11112,7 @@ snapshots:
'@types/node@16.18.126': {}
'@types/node@18.11.0': {}
'@types/node@18.19.121':
'@types/node@18.19.130':
dependencies:
undici-types: 5.26.5
@@ -11127,7 +11122,7 @@ snapshots:
'@types/nodemailer@6.4.17':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/normalize-package-data@2.4.4': {}
@@ -11139,7 +11134,7 @@ snapshots:
'@types/pg@8.15.5':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
pg-protocol: 1.10.3
pg-types: 2.2.0
@@ -11167,22 +11162,22 @@ snapshots:
'@types/readdir-glob@1.1.5':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/request-ip@0.0.37':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/request@2.48.13':
dependencies:
'@types/caseless': 0.12.5
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/tough-cookie': 4.0.5
form-data: 2.5.5
'@types/responselike@1.0.3':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/sanitize-html@2.16.0':
dependencies:
@@ -11194,11 +11189,11 @@ snapshots:
'@types/sharp@0.31.1':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/stream-to-blob@2.0.0':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/tough-cookie@4.0.5': {}
@@ -11220,15 +11215,15 @@ snapshots:
'@types/ws@7.4.7':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/ws@8.18.1':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/xml2js@0.4.14':
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
'@types/youtube-player@5.5.11': {}
@@ -12887,11 +12882,11 @@ snapshots:
string.prototype.matchall: 4.0.12
string.prototype.repeat: 1.0.0
eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2))):
eslint-plugin-tailwindcss@3.18.2(tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2))):
dependencies:
fast-glob: 3.3.3
postcss: 8.5.6
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2))
tailwindcss: 3.4.17(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2))
eslint-scope@5.1.1:
dependencies:
@@ -13967,7 +13962,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -14952,7 +14947,7 @@ snapshots:
openai@4.104.0(ws@8.19.0(bufferutil@4.0.9)(utf-8-validate@5.0.10))(zod@4.0.17):
dependencies:
'@types/node': 18.19.121
'@types/node': 18.19.130
'@types/node-fetch': 2.6.13
abort-controller: 3.0.0
agentkeepalive: 4.6.0
@@ -15235,13 +15230,13 @@ snapshots:
camelcase-css: 2.0.1
postcss: 8.5.6
postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2)):
postcss-load-config@4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2)):
dependencies:
lilconfig: 3.1.3
yaml: 2.8.1
optionalDependencies:
postcss: 8.5.6
ts-node: 10.9.2(@types/node@18.11.0)(typescript@5.9.2)
ts-node: 10.9.2(@types/node@20.19.9)(typescript@5.9.2)
postcss-merge-longhand@7.0.5(postcss@8.5.6):
dependencies:
@@ -15638,7 +15633,7 @@ snapshots:
'@protobufjs/path': 1.1.2
'@protobufjs/pool': 1.1.0
'@protobufjs/utf8': 1.1.0
'@types/node': 20.19.9
'@types/node': 18.19.130
long: 5.3.2
proxy-from-env@1.1.0: {}
@@ -16579,7 +16574,7 @@ snapshots:
stripe@11.18.0:
dependencies:
'@types/node': 20.19.9
'@types/node': 18.19.130
qs: 6.14.0
strnum@2.1.1: {}
@@ -16657,7 +16652,7 @@ snapshots:
tabbable@6.2.0: {}
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2)):
tailwindcss@3.4.17(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2)):
dependencies:
'@alloc/quick-lru': 5.2.0
arg: 5.0.2
@@ -16676,7 +16671,7 @@ snapshots:
postcss: 8.5.6
postcss-import: 15.1.0(postcss@8.5.6)
postcss-js: 4.0.1(postcss@8.5.6)
postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2))
postcss-load-config: 4.0.2(postcss@8.5.6)(ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2))
postcss-nested: 6.2.0(postcss@8.5.6)
postcss-selector-parser: 6.1.2
resolve: 1.22.10
@@ -16830,14 +16825,14 @@ snapshots:
ts-mixer@6.0.4: {}
ts-node@10.9.2(@types/node@18.11.0)(typescript@5.9.2):
ts-node@10.9.2(@types/node@20.19.9)(typescript@5.9.2):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.11
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 18.11.0
'@types/node': 20.19.9
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3
+2 -2
View File
@@ -208,12 +208,12 @@ function SubmissionCountdown(props: BoxProps) {
const endingSoon = timeRemaining < 30 * 1000;
useEffect(() => {
const timer = setInterval(() => {
const timer = window.setInterval(() => {
const newTimeRemaining = end - Date.now();
setTimeRemaining(newTimeRemaining > 0 ? newTimeRemaining : 0);
}, 1000);
return () => clearInterval(timer);
return () => window.clearInterval(timer);
}, [end]);
return (
+3 -3
View File
@@ -38,7 +38,7 @@ function getCountdownString(
type Props = { endTime: Date; refreshIntervalMs?: number; format?: 'short' | 'long' };
export function Countdown({ endTime, refreshIntervalMs = 1000 * 60, format = 'long' }: Props) {
const intervalRef = useRef<NodeJS.Timer>();
const intervalRef = useRef<number>();
const currentTime = dayjs();
const diffTime = dayjs(endTime).unix() - currentTime.unix();
const isClient = useIsClient();
@@ -54,7 +54,7 @@ export function Countdown({ endTime, refreshIntervalMs = 1000 * 60, format = 'lo
useEffect(() => {
if (!intervalRef.current) {
intervalRef.current = setInterval(() => {
intervalRef.current = window.setInterval(() => {
// TODO - clear interval if endTime is less than new date
setTime((duration) => {
const formatted = dayjs.duration(duration.asMilliseconds() - interval, 'milliseconds');
@@ -63,7 +63,7 @@ export function Countdown({ endTime, refreshIntervalMs = 1000 * 60, format = 'lo
}, interval);
}
return () => {
clearInterval(intervalRef.current);
window.clearInterval(intervalRef.current);
intervalRef.current = undefined;
};
}, [endTime, interval]);
@@ -76,7 +76,7 @@ function TwitchStream({ url }: { url: string }) {
useEffect(() => {
if (!ready || initialized) return;
const interval = setInterval(() => {
const interval = window.setInterval(() => {
if (!window.Twitch) return;
new window.Twitch.Embed(id, {
width: '100%',
@@ -88,7 +88,7 @@ function TwitchStream({ url }: { url: string }) {
parent: ['civitai.com'],
});
setInitialized(true);
clearInterval(interval);
window.clearInterval(interval);
}, 100);
}, [ready, id, url, initialized]);
@@ -98,10 +98,10 @@ async function updateSignaledWorkflows() {
function usePollWorkflows() {
const hasIds = usePollableWorkflowIdsStore(({ ids }) => ids.length > 0);
const intervalRef = useRef<NodeJS.Timeout | null>(null);
const intervalRef = useRef<number | null>(null);
function handleClearInterval() {
if (intervalRef.current) {
clearInterval(intervalRef.current);
window.clearInterval(intervalRef.current);
intervalRef.current = null;
}
}
@@ -112,7 +112,7 @@ function usePollWorkflows() {
}
if (!intervalRef.current) {
intervalRef.current = setInterval(async () => {
intervalRef.current = window.setInterval(async () => {
const ids = usePollableWorkflowIdsStore.getState().ids;
await updateWorkflowsStatus(ids);
}, 60000);
@@ -29,13 +29,13 @@ export function PromoNotification({ vendorId, vendorName, promo }: PromoNotifica
return;
}
const checkExpiry = setInterval(() => {
const checkExpiry = window.setInterval(() => {
if (isPromoActive(promo)) return;
setIsVisible(false);
clearInterval(checkExpiry);
window.clearInterval(checkExpiry);
}, 60000); // Check every minute if promo has expired
return () => clearInterval(checkExpiry);
return () => window.clearInterval(checkExpiry);
}, [promo, vendorId]);
if (!promo || !isVisible) {
@@ -208,7 +208,7 @@ async function isLocked(name: string, noCheck?: boolean) {
const LOCK_REFRESH_INTERVAL = 8; // Every 8 seconds
const LOCK_BUFFER = 2; // 2 second buffer on redis expiry
const lockIntervals: Record<string, NodeJS.Timer> = {};
const lockIntervals: Record<string, ReturnType<typeof setInterval>> = {};
async function lock(name: string, lockExpiration: number, noCheck?: boolean) {
if (!isProd || name === 'prepare-leaderboard' || noCheck) return;
logToAxiom({ type: 'job-lock', message: 'lock', job: name }, 'webhooks').catch();
+1 -1
View File
@@ -182,7 +182,7 @@ declare global {
const log = createLogger('redis', 'green');
// Track topology refresh intervals for cleanup
const clusterRefreshIntervals = new Map<string, NodeJS.Timeout>();
const clusterRefreshIntervals = new Map<string, ReturnType<typeof setInterval>>();
/**
* Trigger topology rediscovery on a cluster client.
+1 -1
View File
@@ -29,7 +29,7 @@ export class TaskBatcherIntervals<T> {
private interval: number;
private maxAttempts?: number;
private taskBatcher: TaskBatcher<T>;
private dictionary: Record<string, { interval: NodeJS.Timeout; attempts: number }> = {};
private dictionary: Record<string, { interval: ReturnType<typeof setInterval>; attempts: number }> = {};
configure = ({ callback }: { callback: (batched: T[]) => void }) => {
this.taskBatcher.configure({ callback });
+2 -3
View File
@@ -21,7 +21,7 @@
"paths": {
"~/*": ["./*"]
},
"typeRoots": ["./types"],
"types": ["node"],
"noErrorTruncation": true,
"plugins": [
{
@@ -44,8 +44,7 @@
"node_modules",
"./node_modules",
"./node_modules/*",
"./node_modules/@types/node/index.d.ts",
"src/pages/api/dev-local/*.ts",
"scripts/local-dev/*.ts",
"scripts/local-dev/*.ts"
]
}