More elegant way to generate arrays of keys from a type with full type safety (#4025)

This commit is contained in:
Chris Estreich 2025-05-26 21:27:10 -07:00 • committed by GitHub
parent 1384077495
commit b7d1c9ae4a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 180 additions and 183 deletions

View file

@ -1,4 +1,4 @@
// npx vitest run src/__tests__/index.test.ts
// npx vitest run --globals src/__tests__/index.test.ts
import { GLOBAL_STATE_KEYS } from "../index.js"

View file

@ -1,6 +1,6 @@
import { z } from "zod"
import type { Keys } from "./type-fu.js"
import { type Keys, keysOf } from "./type-fu.js"
import {
type ProviderSettings,
PROVIDER_SETTINGS_KEYS,
@ -34,8 +34,6 @@ export const globalSettingsSchema = z.object({
autoApprovalEnabled: z.boolean().optional(),
alwaysAllowReadOnly: z.boolean().optional(),
alwaysAllowReadOnlyOutsideWorkspace: z.boolean().optional(),
codebaseIndexModels: codebaseIndexModelsSchema.optional(),
codebaseIndexConfig: codebaseIndexConfigSchema.optional(),
alwaysAllowWrite: z.boolean().optional(),
alwaysAllowWriteOutsideWorkspace: z.boolean().optional(),
writeDelayMs: z.number().optional(),
@ -85,6 +83,9 @@ export const globalSettingsSchema = z.object({
fuzzyMatchThreshold: z.number().optional(),
experiments: experimentsSchema.optional(),
codebaseIndexModels: codebaseIndexModelsSchema.optional(),
codebaseIndexConfig: codebaseIndexConfigSchema.optional(),
language: languagesSchema.optional(),
telemetrySetting: telemetrySettingsSchema.optional(),
@ -103,91 +104,87 @@ export const globalSettingsSchema = z.object({
export type GlobalSettings = z.infer<typeof globalSettingsSchema>
type GlobalSettingsRecord = Record<Keys<GlobalSettings>, undefined>
export const GLOBAL_SETTINGS_KEYS = keysOf<GlobalSettings>()([
"currentApiConfigName",
"listApiConfigMeta",
"pinnedApiConfigs",
const globalSettingsRecord: GlobalSettingsRecord = {
codebaseIndexModels: undefined,
codebaseIndexConfig: undefined,
currentApiConfigName: undefined,
listApiConfigMeta: undefined,
pinnedApiConfigs: undefined,
"lastShownAnnouncementId",
"customInstructions",
"taskHistory",
lastShownAnnouncementId: undefined,
customInstructions: undefined,
taskHistory: undefined,
"condensingApiConfigId",
"customCondensingPrompt",
condensingApiConfigId: undefined,
customCondensingPrompt: undefined,
"autoApprovalEnabled",
"alwaysAllowReadOnly",
"alwaysAllowReadOnlyOutsideWorkspace",
"alwaysAllowWrite",
"alwaysAllowWriteOutsideWorkspace",
"writeDelayMs",
"alwaysAllowBrowser",
"alwaysApproveResubmit",
"requestDelaySeconds",
"alwaysAllowMcp",
"alwaysAllowModeSwitch",
"alwaysAllowSubtasks",
"alwaysAllowExecute",
"allowedCommands",
"allowedMaxRequests",
"autoCondenseContextPercent",
autoApprovalEnabled: undefined,
alwaysAllowReadOnly: undefined,
alwaysAllowReadOnlyOutsideWorkspace: undefined,
alwaysAllowWrite: undefined,
alwaysAllowWriteOutsideWorkspace: undefined,
writeDelayMs: undefined,
alwaysAllowBrowser: undefined,
alwaysApproveResubmit: undefined,
requestDelaySeconds: undefined,
alwaysAllowMcp: undefined,
alwaysAllowModeSwitch: undefined,
alwaysAllowSubtasks: undefined,
alwaysAllowExecute: undefined,
allowedCommands: undefined,
allowedMaxRequests: undefined,
autoCondenseContextPercent: undefined,
"browserToolEnabled",
"browserViewportSize",
"screenshotQuality",
"remoteBrowserEnabled",
"remoteBrowserHost",
browserToolEnabled: undefined,
browserViewportSize: undefined,
screenshotQuality: undefined,
remoteBrowserEnabled: undefined,
remoteBrowserHost: undefined,
"enableCheckpoints",
enableCheckpoints: undefined,
"ttsEnabled",
"ttsSpeed",
"soundEnabled",
"soundVolume",
ttsEnabled: undefined,
ttsSpeed: undefined,
soundEnabled: undefined,
soundVolume: undefined,
"maxOpenTabsContext",
"maxWorkspaceFiles",
"showRooIgnoredFiles",
"maxReadFileLine",
maxOpenTabsContext: undefined,
maxWorkspaceFiles: undefined,
showRooIgnoredFiles: undefined,
maxReadFileLine: undefined,
"terminalOutputLineLimit",
"terminalShellIntegrationTimeout",
"terminalShellIntegrationDisabled",
"terminalCommandDelay",
"terminalPowershellCounter",
"terminalZshClearEolMark",
"terminalZshOhMy",
"terminalZshP10k",
"terminalZdotdir",
"terminalCompressProgressBar",
terminalOutputLineLimit: undefined,
terminalShellIntegrationTimeout: undefined,
terminalShellIntegrationDisabled: undefined,
terminalCommandDelay: undefined,
terminalPowershellCounter: undefined,
terminalZshClearEolMark: undefined,
terminalZshOhMy: undefined,
terminalZshP10k: undefined,
terminalZdotdir: undefined,
terminalCompressProgressBar: undefined,
"rateLimitSeconds",
"diffEnabled",
"fuzzyMatchThreshold",
"experiments",
rateLimitSeconds: undefined,
diffEnabled: undefined,
fuzzyMatchThreshold: undefined,
experiments: undefined,
"codebaseIndexModels",
"codebaseIndexConfig",
language: undefined,
"language",
telemetrySetting: undefined,
"telemetrySetting",
"mcpEnabled",
"enableMcpServerCreation",
mcpEnabled: undefined,
enableMcpServerCreation: undefined,
mode: undefined,
modeApiConfigs: undefined,
customModes: undefined,
customModePrompts: undefined,
customSupportPrompts: undefined,
enhancementApiConfigId: undefined,
cachedChromeHostUrl: undefined,
historyPreviewCollapsed: undefined,
}
export const GLOBAL_SETTINGS_KEYS = Object.keys(globalSettingsRecord) as Keys<GlobalSettings>[]
"mode",
"modeApiConfigs",
"customModes",
"customModePrompts",
"customSupportPrompts",
"enhancementApiConfigId",
"cachedChromeHostUrl",
"historyPreviewCollapsed",
])
/**
* RooCodeSettings
@ -224,33 +221,27 @@ export type SecretState = Pick<
| "codeIndexQdrantApiKey"
>
export type CodeIndexSecrets = "codeIndexOpenAiKey" | "codeIndexQdrantApiKey"
type SecretStateRecord = Record<Keys<SecretState>, undefined>
const secretStateRecord: SecretStateRecord = {
apiKey: undefined,
glamaApiKey: undefined,
openRouterApiKey: undefined,
awsAccessKey: undefined,
awsSecretKey: undefined,
awsSessionToken: undefined,
openAiApiKey: undefined,
geminiApiKey: undefined,
openAiNativeApiKey: undefined,
deepSeekApiKey: undefined,
mistralApiKey: undefined,
unboundApiKey: undefined,
requestyApiKey: undefined,
xaiApiKey: undefined,
groqApiKey: undefined,
chutesApiKey: undefined,
litellmApiKey: undefined,
codeIndexOpenAiKey: undefined,
codeIndexQdrantApiKey: undefined,
}
export const SECRET_STATE_KEYS = Object.keys(secretStateRecord) as Keys<SecretState>[]
export const SECRET_STATE_KEYS = keysOf<SecretState>()([
"apiKey",
"glamaApiKey",
"openRouterApiKey",
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"openAiApiKey",
"geminiApiKey",
"openAiNativeApiKey",
"deepSeekApiKey",
"mistralApiKey",
"unboundApiKey",
"requestyApiKey",
"xaiApiKey",
"groqApiKey",
"chutesApiKey",
"litellmApiKey",
"codeIndexOpenAiKey",
"codeIndexQdrantApiKey",
])
export const isSecretStateKey = (key: string): key is Keys<SecretState> =>
SECRET_STATE_KEYS.includes(key as Keys<SecretState>)

View file

@ -1,6 +1,6 @@
import { z } from "zod"
import type { Keys } from "./type-fu.js"
import { keysOf } from "./type-fu.js"
import { reasoningEffortsSchema, modelInfoSchema } from "./model.js"
import { codebaseIndexProviderSchema } from "./codebase-index.js"
@ -257,104 +257,100 @@ export const providerSettingsSchema = z.object({
export type ProviderSettings = z.infer<typeof providerSettingsSchema>
type ProviderSettingsRecord = Record<Keys<ProviderSettings>, undefined>
const providerSettingsRecord: ProviderSettingsRecord = {
apiProvider: undefined,
export const PROVIDER_SETTINGS_KEYS = keysOf<ProviderSettings>()([
"apiProvider",
// Anthropic
apiModelId: undefined,
apiKey: undefined,
anthropicBaseUrl: undefined,
anthropicUseAuthToken: undefined,
"apiModelId",
"apiKey",
"anthropicBaseUrl",
"anthropicUseAuthToken",
// Glama
glamaModelId: undefined,
glamaApiKey: undefined,
"glamaModelId",
"glamaApiKey",
// OpenRouter
openRouterApiKey: undefined,
openRouterModelId: undefined,
openRouterBaseUrl: undefined,
openRouterSpecificProvider: undefined,
openRouterUseMiddleOutTransform: undefined,
"openRouterApiKey",
"openRouterModelId",
"openRouterBaseUrl",
"openRouterSpecificProvider",
"openRouterUseMiddleOutTransform",
// Amazon Bedrock
awsAccessKey: undefined,
awsSecretKey: undefined,
awsSessionToken: undefined,
awsRegion: undefined,
awsUseCrossRegionInference: undefined,
awsUsePromptCache: undefined,
awsProfile: undefined,
awsUseProfile: undefined,
awsCustomArn: undefined,
"awsAccessKey",
"awsSecretKey",
"awsSessionToken",
"awsRegion",
"awsUseCrossRegionInference",
"awsUsePromptCache",
"awsProfile",
"awsUseProfile",
"awsCustomArn",
// Google Vertex
vertexKeyFile: undefined,
vertexJsonCredentials: undefined,
vertexProjectId: undefined,
vertexRegion: undefined,
"vertexKeyFile",
"vertexJsonCredentials",
"vertexProjectId",
"vertexRegion",
// OpenAI
openAiBaseUrl: undefined,
openAiApiKey: undefined,
openAiLegacyFormat: undefined,
openAiR1FormatEnabled: undefined,
openAiModelId: undefined,
openAiCustomModelInfo: undefined,
openAiUseAzure: undefined,
azureApiVersion: undefined,
openAiStreamingEnabled: undefined,
openAiHostHeader: undefined, // Keep temporarily for backward compatibility during migration
openAiHeaders: undefined,
"openAiBaseUrl",
"openAiApiKey",
"openAiLegacyFormat",
"openAiR1FormatEnabled",
"openAiModelId",
"openAiCustomModelInfo",
"openAiUseAzure",
"azureApiVersion",
"openAiStreamingEnabled",
"openAiHostHeader", // Keep temporarily for backward compatibility during migration.
"openAiHeaders",
// Ollama
ollamaModelId: undefined,
ollamaBaseUrl: undefined,
"ollamaModelId",
"ollamaBaseUrl",
// VS Code LM
vsCodeLmModelSelector: undefined,
lmStudioModelId: undefined,
lmStudioBaseUrl: undefined,
lmStudioDraftModelId: undefined,
lmStudioSpeculativeDecodingEnabled: undefined,
"vsCodeLmModelSelector",
"lmStudioModelId",
"lmStudioBaseUrl",
"lmStudioDraftModelId",
"lmStudioSpeculativeDecodingEnabled",
// Gemini
geminiApiKey: undefined,
googleGeminiBaseUrl: undefined,
"geminiApiKey",
"googleGeminiBaseUrl",
// OpenAI Native
openAiNativeApiKey: undefined,
openAiNativeBaseUrl: undefined,
"openAiNativeApiKey",
"openAiNativeBaseUrl",
// Mistral
mistralApiKey: undefined,
mistralCodestralUrl: undefined,
"mistralApiKey",
"mistralCodestralUrl",
// DeepSeek
deepSeekBaseUrl: undefined,
deepSeekApiKey: undefined,
"deepSeekBaseUrl",
"deepSeekApiKey",
// Unbound
unboundApiKey: undefined,
unboundModelId: undefined,
"unboundApiKey",
"unboundModelId",
// Requesty
requestyApiKey: undefined,
requestyModelId: undefined,
"requestyApiKey",
"requestyModelId",
// Code Index
codeIndexOpenAiKey: undefined,
codeIndexQdrantApiKey: undefined,
"codeIndexOpenAiKey",
"codeIndexQdrantApiKey",
// Reasoning
enableReasoningEffort: undefined,
reasoningEffort: undefined,
modelMaxTokens: undefined,
modelMaxThinkingTokens: undefined,
"enableReasoningEffort",
"reasoningEffort",
"modelMaxTokens",
"modelMaxThinkingTokens",
// Generic
includeMaxTokens: undefined,
diffEnabled: undefined,
fuzzyMatchThreshold: undefined,
modelTemperature: undefined,
rateLimitSeconds: undefined,
"includeMaxTokens",
"diffEnabled",
"fuzzyMatchThreshold",
"modelTemperature",
"rateLimitSeconds",
// Fake AI
fakeAi: undefined,
"fakeAi",
// X.AI (Grok)
xaiApiKey: undefined,
"xaiApiKey",
// Groq
groqApiKey: undefined,
"groqApiKey",
// Chutes AI
chutesApiKey: undefined,
"chutesApiKey",
// LiteLLM
litellmBaseUrl: undefined,
litellmApiKey: undefined,
litellmModelId: undefined,
}
export const PROVIDER_SETTINGS_KEYS = Object.keys(providerSettingsRecord) as Keys<ProviderSettings>[]
"litellmBaseUrl",
"litellmApiKey",
"litellmModelId",
])

View file

@ -9,3 +9,13 @@ export type Values<T> = T[keyof T]
export type Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2 ? true : false
export type AssertEqual<T extends true> = T
/**
* Creates a type-safe keys array that enforces ALL keys from type T are present.
* Returns a compile-time error if any keys are missing or extra keys are provided.
*/
export function keysOf<T>() {
return <const U extends readonly (keyof T)[]>(
keys: keyof T extends U[number] ? (U[number] extends keyof T ? U : never) : never,
): U => keys
}