```ts
/**
 * Parses a semantic version string into its constituent parts.
 *
 * @param version The semantic version string to parse.
 * @throws {Error} If the input is not a valid semantic version string.
 */
export function parseSemver(version: string): {
  major: number;
  minor: number;
  patch: number;
  prerelease: string | null;
  build: string | null;
} {
  const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?(\+.+)?$/);
  if (!match) {
    throw new Error("Invalid semantic version string");
  }

  const [, major, minor, patch, prerelease, build] =
    match.slice(1).map((value) => parseFloat(value.replace(/^-|\+$/, '')));

  return {
    major,
    minor,
    patch,
    prerelease: prerelease ? prerelease : null,
    build: build ? build : null,
  };
}
```