{"record":{"id":"f3c52d11333ec921","repo":"actualbudget/actual","slug":"unrecognised-link-variant","errorCode":null,"errorMessage":"Unrecognised Link variant.","messagePattern":"Unrecognised Link variant\\.","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/desktop-client/src/components/common/Link.tsx","lineNumber":177,"sourceCode":"    }\n\n    case 'external': {\n      const { variant: _, ...externalProps } = props;\n      return <ExternalLink {...externalProps} />;\n    }\n\n    case 'button': {\n      const { variant: _, ...buttonProps } = props;\n      return <ButtonLink {...buttonProps} />;\n    }\n\n    case 'text': {\n      const { variant: _, ...textProps } = props;\n      return <TextLink {...textProps} />;\n    }\n\n    default:\n      throw new Error(`Unrecognised Link variant.`);\n  }\n}\n","sourceCodeStart":159,"sourceCodeEnd":180,"githubUrl":"https://github.com/actualbudget/actual/blob/d4334cb6e6123f4d3bcea1ad6166608884c7e658/packages/desktop-client/src/components/common/Link.tsx#L159-L180","documentation":"The common Link component is a discriminated union on `variant` ('button' | 'internal' | 'external' | 'text') and its switch throws on anything else. Since TypeScript should make the default branch unreachable for typed callers, this fires only when variant is undefined/invalid — typically from untyped JavaScript callers, an any-typed props spread, or data-driven code passing a string from config/API.","triggerScenarios":"Rendering <Link> without a `variant` prop (variant === undefined), or passing a value outside 'button'|'internal'|'external'|'text' (e.g. 'url', 'anchor', a value read from JSON/props of type any), bypassing the LinkProps union type.","commonSituations":"Migrating from an older Link API that defaulted variant; mapping backend/config strings directly to <Link variant={someString}>; JS files (or .tsx files with @ts-strict-ignore) that let an invalid variant through type checking.","solutions":["Pass a valid variant: one of 'button', 'internal', 'external', or 'text'.","If the variant comes from data, validate/coerce it before rendering (whitelist check with a fallback like 'external').","Fix the caller's typing so TS enforces the LinkProps union — avoid `as any`/any-typed spreads that hide the missing variant.","If a no-variant default is desired, render a sensible fallback (e.g. external or text link) in the default branch instead of throwing."],"exampleFix":"// before\n<Link to=\"https://example.com\">Docs</Link> // no variant -> throws\n\n// after\n<Link variant=\"external\" to=\"https://example.com\">Docs</Link>","handlingStrategy":"type-guard","validationCode":"const LINK_VARIANTS = ['button', 'internal', 'external', 'text'] as const;\nconst safeVariant = LINK_VARIANTS.includes(rawVariant as any) ? rawVariant : 'external';\n<Link variant={safeVariant} to={to}>...</Link>","typeGuard":"function isLinkVariant(v: unknown): v is 'button' | 'internal' | 'external' | 'text' {\n  return v === 'button' || v === 'internal' || v === 'external' || v === 'text';\n}","tryCatchPattern":"let link;\ntry {\n  link = <Link variant={variant as any} to={to}>{children}</Link>;\n} catch (err) {\n  if (err instanceof Error && err.message.includes('Unrecognised Link variant')) {\n    link = <Link variant=\"external\" to={to}>{children}</Link>;\n  } else {\n    throw err;\n  }\n}","preventionTips":["Always pass an explicit `variant` prop to Link.","Validate/coerce variant strings coming from config, CMS data, or any-typed sources with isLinkVariant before rendering.","Avoid `as any` prop spreads that bypass the LinkProps discriminated union."],"tags":["react","typescript","discriminated-union","invalid-props"],"backgroundTag":"invalid-prop-value","analyzedSha":"d4334cb6e6123f4d3bcea1ad6166608884c7e658","analyzedAt":"2026-08-29T01:02:11.213Z","schemaVersion":2},"datasetVersion":"2026-08-29T02:17:18.158Z"}