Make type signature of groupBy more accurate (#272395)

See #272263, it currently assumes that all keys of K will be defined. This still isn't perfect, because it says that the returned object might contain a key with value `undefined`, but in reality, if a key is defined, it will have a value. So now usages of this with Object.entries and so on are slighly wrong, but that's preferable IMO.
This commit is contained in:
Rob Lourens
2025-10-20 21:13:56 -07:00
committed by GitHub
parent 0809a8ebdb
commit b9480e9d7d
9 changed files with 24 additions and 16 deletions

View File

@@ -19,8 +19,8 @@ export type INumberDictionary<V> = Record<number, V>;
* Groups the collection into a dictionary based on the provided
* group function.
*/
export function groupBy<K extends string | number | symbol, V>(data: readonly V[], groupFn: (element: V) => K): Record<K, V[]> {
const result: Record<K, V[]> = Object.create(null);
export function groupBy<K extends string | number | symbol, V>(data: readonly V[], groupFn: (element: V) => K): Partial<Record<K, V[]>> {
const result: Partial<Record<K, V[]>> = Object.create(null);
for (const element of data) {
const key = groupFn(element);
let target = result[key];

View File

@@ -206,7 +206,9 @@ export class DisposableTracker implements IDisposableTracker {
const continuations = groupBy([...prevStarts].map(d => getStackTracePath(d)[i]), v => v);
delete continuations[stackTracePath[i]];
for (const [cont, set] of Object.entries(continuations)) {
stackTraceFormattedLines.unshift(` - stacktraces of ${set.length} other leaks continue with ${cont}`);
if (set) {
stackTraceFormattedLines.unshift(` - stacktraces of ${set.length} other leaks continue with ${cont}`);
}
}
stackTraceFormattedLines.unshift(line);

View File

@@ -24,12 +24,12 @@ suite('Collections', () => {
const grouped = collections.groupBy(source, x => x.key);
// Group 1
assert.strictEqual(grouped[group1].length, 2);
assert.strictEqual(grouped[group1]?.length, 2);
assert.strictEqual(grouped[group1][0].value, value1);
assert.strictEqual(grouped[group1][1].value, value2);
// Group 2
assert.strictEqual(grouped[group2].length, 1);
assert.strictEqual(grouped[group2]?.length, 1);
assert.strictEqual(grouped[group2][0].value, value3);
});

View File

@@ -79,11 +79,11 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId ?
'builtin' : 'custom');
const customBuiltinModeActions = customModes.builtin.map(mode => {
const customBuiltinModeActions = customModes.builtin?.map(mode => {
const action = makeActionFromCustomMode(mode, currentMode);
action.category = builtInCategory;
return action;
});
}) ?? [];
const orderedModes = coalesce([
agentMode && makeAction(agentMode, currentMode),

View File

@@ -410,9 +410,9 @@ export class BaseIssueReporterService extends Disposable {
return ext.isTheme ? 'themes' : 'nonThemes';
});
const numberOfThemeExtesions = themes && themes.length;
const numberOfThemeExtesions = (themes && themes.length) ?? 0;
this.issueReporterModel.update({ numberOfThemeExtesions, enabledNonThemeExtesions: nonThemes, allExtensions: installedExtensions });
this.updateExtensionTable(nonThemes, numberOfThemeExtesions);
this.updateExtensionTable(nonThemes ?? [], numberOfThemeExtesions);
if (this.disableExtensions || installedExtensions.length === 0) {
(<HTMLButtonElement>this.getElementById('disableExtensions')).disabled = true;
}

View File

@@ -19,9 +19,11 @@ class TestMarkersModel extends MarkersModel {
Object.keys(byResource).forEach(key => {
const markers = byResource[key];
const resource = markers[0].resource;
if (markers) {
const resource = markers[0].resource;
this.setResourceMarkers([[resource, markers]]);
this.setResourceMarkers([[resource, markers]]);
}
});
}
}

View File

@@ -117,9 +117,9 @@ export class ListMcpServerCommand extends Action2 {
const firstRun = pick.items.length === 0;
pick.items = [
{ id: '$add', label: localize('mcp.addServer', 'Add Server'), description: localize('mcp.addServer.description', 'Add a new server configuration'), alwaysShow: true, iconClass: ThemeIcon.asClassName(Codicon.add) },
...Object.values(servers).filter(s => s.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [
{ type: 'separator', label: servers[0].collection.label, id: servers[0].collection.id },
...servers.map(server => ({
...Object.values(servers).filter(s => s!.length).flatMap((servers): (ItemType | IQuickPickSeparator)[] => [
{ type: 'separator', label: servers![0].collection.label, id: servers![0].collection.id },
...servers!.map(server => ({
id: server.definition.id,
label: server.definition.label,
description: McpConnectionState.toString(server.connectionState.read(reader)),

View File

@@ -778,7 +778,7 @@ export class NotebookViewModel extends Disposable implements EditorFoldingStateD
for (const _handle in deletesByHandle) {
const handle = parseInt(_handle);
const ids = deletesByHandle[handle];
const ids = deletesByHandle[handle]!;
const cell = this.getCellByHandle(handle);
cell?.deltaCellStatusBarItems(ids, []);
ids.forEach(id => this._statusBarItemIdToCellMap.delete(id));

View File

@@ -504,11 +504,15 @@ class HistoryItemRenderer implements ICompressibleTreeRenderer<SCMHistoryItemVie
continue;
}
if (!historyItemRefs) {
continue;
}
// Group history item references by icon
const historyItemRefByIconId = groupBy2(historyItemRefs, ref => ThemeIcon.isThemeIcon(ref.icon) ? ref.icon.id : '');
for (const [key, historyItemRefs] of Object.entries(historyItemRefByIconId)) {
// Skip badges without an icon
if (key === '') {
if (key === '' || !historyItemRefs) {
continue;
}