Skip to content

Commit 812393a

Browse files
authored
Migrate to TypeScript v5 (#2850)
* Bump TypeScript to v5.4 Also updated tsconfig.json to remove a deprecated config. * Fix type error * Fix type error * Fix type error * Fix type error * Fix type error * Fix lint dependencies and config * Fix type errors * Fix type error * Fix format * Fix type errors Also standardized React hooks import style. * Fix type error * Fix type error * Fix format * Bump typescript to v5.4.3 * Fix type errors * Add some `as any` assertions FIXMEs were also incorporated for future refactoring. * Update lockfile post-merge * Bump dependencies * Update lockfile post-merge * Create type-safe object helpers * Fix type error * Fix type error * Fix type error * Create more type helpers * Fix type error * Fix type error Also removed unnecessary type annotation. * Fix type error * Fix type error * Fix type errors * Fix type error * Fix type error * Fix type errors * Fix type error * Fix type error * Refactor filter logic to fix type error * Fix types * Address comments
1 parent 87e646d commit 812393a

File tree

31 files changed

+461
-230
lines changed

31 files changed

+461
-230
lines changed

.eslintrc.json

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
{
2-
"extends": ["react-app", "plugin:@typescript-eslint/recommended"],
2+
"extends": [
3+
// "eslint:recommended",
4+
"plugin:@typescript-eslint/recommended",
5+
"plugin:react-hooks/recommended"
6+
// "plugin:react/recommended",
7+
// "plugin:react/jsx-runtime"
8+
],
39
"plugins": ["simple-import-sort"],
410
"rules": {
511
"no-restricted-imports": [
@@ -18,6 +24,8 @@
1824
]
1925
}
2026
],
27+
"@typescript-eslint/no-unused-vars": "off",
28+
"@typescript-eslint/no-duplicate-enum-values": "off",
2129
"@typescript-eslint/no-empty-function": "off",
2230
"@typescript-eslint/interface-name-prefix": "off",
2331
"@typescript-eslint/camelcase": "off",

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,16 @@
127127
"@types/showdown": "^2.0.1",
128128
"@types/uuid": "^9.0.0",
129129
"@types/xml2js": "^0.4.11",
130+
"@typescript-eslint/eslint-plugin": "^7.4.0",
131+
"@typescript-eslint/parser": "^7.4.0",
130132
"babel-core": "6",
131133
"babel-runtime": "^6.26.0",
132134
"buffer": "^6.0.3",
133135
"canvas": "^2.11.2",
134136
"constants-browserify": "^1.0.0",
135137
"coveralls": "^3.1.1",
136138
"cross-env": "^7.0.3",
139+
"eslint": "^8.57.0",
137140
"eslint-plugin-simple-import-sort": "^12.0.0",
138141
"https-browserify": "^1.0.0",
139142
"husky": "^9.0.0",
@@ -151,7 +154,7 @@
151154
"stream-browserify": "^3.0.0",
152155
"stream-http": "^3.2.0",
153156
"timers-browserify": "^2.0.12",
154-
"typescript": "~4.9.0",
157+
"typescript": "^5.4.3",
155158
"url": "^0.11.1",
156159
"webpack-bundle-analyzer": "^4.9.0"
157160
},

src/commons/XMLParser/XMLParserHelper.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,11 @@ const makeQuestions = (task: XmlParseStrTask): [Question[], number] => {
167167

168168
const makeMCQ = (problem: XmlParseStrCProblem, question: BaseQuestion): IMCQQuestion => {
169169
const choicesVal: MCQChoice[] = [];
170-
const solution = problem.SNIPPET ? problem.SNIPPET[0].SOLUTION : undefined;
170+
const snippet = problem.SNIPPET;
171+
// FIXME: I think `XmlParseStrCProblem` type definition is incorrect
172+
// FIXME: Remove `as unknown as keyof typeof snippet` when fixed
173+
// @ts-expect-error broken type definition to be fixed above
174+
const solution = snippet ? snippet[0 as unknown as keyof typeof snippet].SOLUTION : undefined;
171175
let solutionVal = 0;
172176
problem.CHOICE.forEach((choice: XmlParseStrProblemChoice, i: number) => {
173177
choicesVal.push({
@@ -269,7 +273,8 @@ const exportLibrary = (library: Library) => {
269273
name: library.external.name
270274
}
271275
}
272-
};
276+
// FIXME: Replace any with proper type
277+
} as any;
273278

274279
if (library.external.symbols.length !== 0) {
275280
/* tslint:disable:no-string-literal */
@@ -327,7 +332,8 @@ export const assessmentToXml = (
327332
},
328333
TEXT: question.content,
329334
CHOICE: [] as any[]
330-
};
335+
// FIXME: Replace any with proper type
336+
} as any;
331337

332338
if (question.library.chapter !== -1) {
333339
/* tslint:disable:no-string-literal */

src/commons/collabEditing/CollabEditingHelper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
import Constants from '../utils/Constants';
22

3-
const protocolMap = {
3+
const protocolMap = Object.freeze({
44
'http:': 'ws:',
55
'https:': 'wss:'
6-
};
6+
});
77

88
export function getSessionUrl(sessionId: string, ws?: boolean): string {
99
const url = new URL(sessionId, Constants.sharedbBackendUrl);
10-
if (ws) {
11-
url.protocol = protocolMap[url.protocol];
10+
if (ws && Object.keys(protocolMap).includes(url.protocol)) {
11+
url.protocol = protocolMap[url.protocol as keyof typeof protocolMap];
1212
}
1313
return url.toString();
1414
}

src/commons/documentation/Documentation.ts

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,14 @@ import { deviceTypes } from 'src/features/remoteExecution/RemoteExecutionTypes';
33

44
import { externalLibraries } from '../application/types/ExternalTypes';
55

6-
const externalLibrariesDocumentation = {};
6+
type DocType = {
7+
caption: string;
8+
value: string;
9+
meta: string;
10+
docHTML?: string;
11+
};
12+
13+
const externalLibrariesDocumentation: Record<string, DocType[]> = {};
714

815
const MAX_CAPTION_LENGTH = 27;
916

@@ -15,13 +22,14 @@ function shortenCaption(name: string): string {
1522
return (name = name.substring(0, MAX_CAPTION_LENGTH - 3) + '...');
1623
}
1724

18-
function mapExternalLibraryName(name: string) {
25+
function mapExternalLibraryName(name: string): DocType {
1926
if (name in SourceDocumentation.ext_lib) {
27+
const key = name as keyof typeof SourceDocumentation.ext_lib;
2028
return {
21-
caption: shortenCaption(name),
22-
value: name,
23-
meta: SourceDocumentation.ext_lib[name].meta,
24-
docHTML: SourceDocumentation.ext_lib[name].description
29+
caption: shortenCaption(key),
30+
value: key,
31+
meta: SourceDocumentation.ext_lib[key].meta,
32+
docHTML: SourceDocumentation.ext_lib[key].description
2533
};
2634
} else {
2735
return {
@@ -42,7 +50,7 @@ for (const deviceType of deviceTypes) {
4250
deviceType.internalFunctions.map(mapExternalLibraryName);
4351
}
4452

45-
const builtinDocumentation = {};
53+
const builtinDocumentation: Record<string, DocType[]> = {};
4654

4755
Object.entries(SourceDocumentation.builtins).forEach((chapterDoc: any) => {
4856
const [chapter, docs] = chapterDoc;

src/commons/editingWorkspace/EditingWorkspace.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -723,7 +723,7 @@ const EditingWorkspace: React.FC<EditingWorkspaceProps> = props => {
723723
};
724724

725725
function uniq(a: string[]) {
726-
const seen = {};
726+
const seen: Record<string, boolean> = {};
727727
return a.filter(item => (seen.hasOwnProperty(item) ? false : (seen[item] = true)));
728728
}
729729

src/commons/editor/Editor.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { IAceEditor } from 'react-ace/lib/types';
1515
import { HotKeys } from 'react-hotkeys';
1616
import { EditorBinding } from '../WorkspaceSettingsContext';
1717
import { getModeString, selectMode } from '../utils/AceHelper';
18+
import { objectEntries } from '../utils/TypeHelper';
1819
import { KeyFunction, keyBindings } from './EditorHotkeys';
1920
import { AceMouseEvent, HighlightedLines, Position } from './EditorTypes';
2021

@@ -537,7 +538,7 @@ const EditorBase = React.memo((props: EditorProps & LocalStateProps) => {
537538
]
538539
);
539540

540-
aceEditorProps.commands = Object.entries(keyHandlers)
541+
aceEditorProps.commands = objectEntries(keyHandlers)
541542
.filter(([_, exec]) => exec)
542543
.map(([name, exec]) => ({ name, bindKey: keyBindings[name], exec: exec! }));
543544

src/commons/editor/tabs/utils.ts

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ export const getShortestUniqueFilePaths = (originalFilePaths: string[]): string[
2727
// Split each original file path into path segments and store the mapping from file
2828
// path to path segments for O(1) lookup. Since we only deal with the BrowserFS file
2929
// system, the path separator will always be '/'.
30-
const filePathSegments: Record<string, string[]> = originalFilePaths.reduce(
30+
const filePathSegments: Record<string, string[]> = originalFilePaths.reduce<
31+
typeof filePathSegments
32+
>(
3133
(segments, filePath) => ({
3234
...segments,
3335
// It is necessary to remove empty segments to deal with the very first '/' in
@@ -48,15 +50,18 @@ export const getShortestUniqueFilePaths = (originalFilePaths: string[]): string[
4850
// to any original file path which transforms into it.
4951
const shortenedToOriginalFilePaths: Record<string, string[]> = Object.entries(
5052
filePathSegments
51-
).reduce((filePaths, [originalFilePath, filePathSegments]) => {
52-
// Note that if there are fewer path segments than the number being sliced,
53-
// all of the path segments will be returned without error.
54-
const shortenedFilePath = '/' + filePathSegments.slice(-numOfPathSegments).join('/');
55-
return {
56-
...filePaths,
57-
[shortenedFilePath]: (filePaths[shortenedFilePath] ?? []).concat(originalFilePath)
58-
};
59-
}, {});
53+
).reduce<typeof shortenedToOriginalFilePaths>(
54+
(filePaths, [originalFilePath, filePathSegments]) => {
55+
// Note that if there are fewer path segments than the number being sliced,
56+
// all of the path segments will be returned without error.
57+
const shortenedFilePath = '/' + filePathSegments.slice(-numOfPathSegments).join('/');
58+
return {
59+
...filePaths,
60+
[shortenedFilePath]: (filePaths[shortenedFilePath] ?? []).concat(originalFilePath)
61+
};
62+
},
63+
{}
64+
);
6065
// Each shortened file path that only has a single corresponding original file
6166
// path is added to the unique shortened file paths record and their entry in
6267
// the file path segments record is removed to prevent further processing.

src/commons/sagas/WorkspaceSaga/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -490,7 +490,7 @@ export default function* WorkspaceSaga(): SagaIterator {
490490
yield call([CseMachine, CseMachine.clear]);
491491
const globals: Array<[string, any]> = action.payload.library.globals as Array<[string, any]>;
492492
for (const [key, value] of globals) {
493-
window[key] = value;
493+
window[key as any] = value;
494494
}
495495
yield put(
496496
actions.endClearContext(

src/commons/sideContent/SideContentHelper.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ const requireProvider = (x: string) => {
4040
};
4141

4242
if (!(x in exports)) throw new Error(`Dynamic require of ${x} is not supported`);
43-
return exports[x];
43+
return exports[x as keyof typeof exports] as any;
4444
};
4545

4646
type RawTab = (provider: ReturnType<typeof requireProvider>) => { default: ModuleSideContent };

src/commons/sideContent/content/SideContentContestVoting.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,8 @@ const SideContentContestVoting: React.FC<SideContentContestVotingProps> = ({
9797
[currentDraggedItem]
9898
);
9999

100-
const contestEntryRefs = useRef({});
101-
const tierContainerRefs = useRef({});
100+
const contestEntryRefs = useRef<Record<number, HTMLDivElement | null>>({});
101+
const tierContainerRefs = useRef<Record<number, HTMLDivElement | null>>({});
102102

103103
const tierBoard = useMemo(() => {
104104
return TIERS.map((tier, index) => (

src/commons/utils/DisplayBufferService.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,17 @@ class BufferService {
2222

2323
public attachConsole(workspaceLocation: WorkspaceLocation): () => void {
2424
const bufferCallback = (log: string) => this.push(log, workspaceLocation);
25-
const defaultConsole = {};
25+
const defaultConsole: Record<string, any> = {};
2626
Object.entries(consoleOverloads).forEach(([method, overload]) => {
27-
defaultConsole[method] = console[method];
28-
console[method] = overload(bufferCallback);
27+
const key = method as keyof typeof consoleOverloads;
28+
defaultConsole[method] = console[key];
29+
console[key] = overload(bufferCallback);
2930
});
3031

3132
return () => {
3233
Object.entries(consoleOverloads).forEach(([method]) => {
33-
console[method] = defaultConsole[method];
34+
const key = method as keyof typeof consoleOverloads;
35+
console[key] = defaultConsole[key];
3436
});
3537
};
3638
}

src/commons/utils/JsSlangHelper.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export function makeElevatedContext(context: Context) {
180180
if (prop === 'head') {
181181
return fakeFrame;
182182
}
183-
return target[prop];
183+
return target[prop as keyof typeof target];
184184
}
185185
});
186186

@@ -189,7 +189,7 @@ export function makeElevatedContext(context: Context) {
189189
if (prop === '0') {
190190
return proxyGlobalEnv;
191191
}
192-
return target[prop];
192+
return target[prop as keyof typeof target];
193193
}
194194
});
195195

@@ -198,7 +198,7 @@ export function makeElevatedContext(context: Context) {
198198
if (prop === 'environments') {
199199
return proxyEnvs;
200200
}
201-
return target[prop];
201+
return target[prop as keyof typeof target];
202202
}
203203
});
204204

@@ -210,7 +210,7 @@ export function makeElevatedContext(context: Context) {
210210
case 'runtime':
211211
return proxyRuntime;
212212
default:
213-
return target[prop];
213+
return target[prop as keyof typeof target];
214214
}
215215
}
216216
});

src/commons/utils/TypeHelper.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,32 @@ export type KeysOfType<O, T> = {
1010
[K in keyof O]: O[K] extends T ? K : never;
1111
}[keyof O];
1212

13+
/**
14+
* Does union(keyof <member>) for each member of T. This is unlike
15+
* `keyof T` which would give keyof(union(<member>)).
16+
* @param T - The union type to extract keys from
17+
*/
18+
export type DistributedKeyOf<T extends Record<any, any>> = T extends any ? keyof T : never;
19+
20+
/**
21+
* Generates a "set difference" of two types, keeping the properties of T that are not
22+
* present in S.
23+
* @param T - The type to extract keys from
24+
* @param S - The type to compare against
25+
*/
26+
export type Diff<T extends Record<any, any>, S extends Record<any, any>> = Pick<
27+
T,
28+
Exclude<keyof T, keyof S>
29+
>;
30+
31+
/**
32+
* Merges two types together, keeping the properties of T and adding the
33+
* properties of U that are not present in T.
34+
* @param T - The first type
35+
* @param U - The second type (also "universal" set of properties to add to T)
36+
*/
37+
export type Merge<T extends Record<any, any>, U extends Record<any, any>> = T & Diff<U, T>;
38+
1339
// Adapted from https://github.com/piotrwitek/typesafe-actions/blob/a1fe54bb150ac1b935bb9ca78361d2d024d2efaf/src/type-helpers.ts#L117-L130
1440
export type ActionType<T extends Record<string, any>> = {
1541
[k in keyof T]: ReturnType<T[k]>;
@@ -100,3 +126,26 @@ export const assertType =
100126
// Keep the original type as inferred by TS
101127
): T =>
102128
obj;
129+
130+
/**
131+
* Type safe `Object.keys`
132+
*/
133+
export function objectKeys<T extends string | number | symbol>(obj: Record<T, any>): T[] {
134+
return Object.keys(obj) as T[];
135+
}
136+
137+
/**
138+
* Type safe `Object.values`
139+
*/
140+
export function objectValues<T>(obj: Record<any, T>) {
141+
return Object.values(obj) as T[];
142+
}
143+
144+
/**
145+
* Type safe `Object.entries`
146+
*/
147+
export function objectEntries<K extends string | number | symbol, V>(
148+
obj: Partial<Record<K, V>>
149+
): [K, V][] {
150+
return Object.entries(obj) as [K, V][];
151+
}

0 commit comments

Comments
 (0)