A catch block's error value is not typed to Error, and TypeScript will not let you assert that it is. What follows is the practical path from the error to a usable message.

const reportError = ({ message }) => {
	// send the error to our logging service...
}

try {
	throw new Error('Oh no!')
} catch (error) {
	// we'll proceed, but let's report it
	reportError({ message: error.message })
}
That compiles as JavaScript without complaint. Under TypeScript it does not:
const reportError = ({ message }: { message: string }) => {
	// send the error to our logging service...
}

try {
	throw new Error('Oh no!')
} catch (error) {
	// we'll proceed, but let's report it
	reportError({ message: error.message })
}
The offending expression is error.message. TypeScript now defaults the variable in a catch clause to unknown, which reflects reality: a thrown value carries no guarantees. The same reasoning explains why a promise's rejection type cannot be supplied through the generic parameter — Promise<ResolvedValue, NopeYouCantProvideARejectedValueType> — and why the parameter of .catch(error => {}) has to stay untyped. A throw isn't even required to carry an error:
throw 'What the!?'
throw 7
throw { wut: 'is this' }
throw null
throw new Promise(() => {})
throw undefined

Annotating the catch variable doesn't work

Since anything can be thrown, the tempting fix is to annotate the variable so only Error is allowed through:
try {
	throw new Error('Oh no!')
} catch (error: Error) {
	// we'll proceed, but let's report it
	reportError({ message: error.message })
}
TypeScript rejects that outright:
Catch clause variable type annotation must be 'any' or 'unknown' if specified. ts(1196)
Your code may look incapable of throwing anything else, but JavaScript permits third-party code to do exactly that — for instance by monkey-patching the error constructor to throw a different value:
Error = function () {
	throw 'Flowers'
} as any

Narrowing instead of asserting

The alternative is to narrow at runtime, which keeps the compiler satisfied and covers the genuinely unexpected cases:
try {
	throw new Error('Oh no!')
} catch (error) {
	let message = 'Unknown Error'
	if (error instanceof Error) message = error.message
	// we'll proceed, but let's report it
	reportError({ message })
}
This can be taken one step further: when the value is not an Error, stringify it so the message still has a chance of being useful.
try {
	throw new Error('Oh no!')
} catch (error) {
	let message
	if (error instanceof Error) message = error.message
	else message = String(error)
	// we'll proceed, but let's report it
	reportError({ message })
}

Packaging it as a utility

The pattern is repetitive enough to live in a shared helper for every catch block:
function getErrorMessage(error: unknown) {
	if (error instanceof Error) return error.message
	return String(error)
}

const reportError = ({ message }: { message: string }) => {
	// send the error to our logging service...
}

try {
	throw new Error('Oh no!')
} catch (error) {
	// we'll proceed, but let's report it
	reportError({ message: getErrorMessage(error) })
}
Two review suggestions shaped the final version: handling the case where the value isn't an actual error object, and stringifying the error object where that is possible. Combined, they produce this:
type ErrorWithMessage = {
	message: string
}

function isErrorWithMessage(error: unknown): error is ErrorWithMessage {
	return (
		typeof error === 'object' &&
		error !== null &&
		'message' in error &&
		typeof (error as Record<string, unknown>).message === 'string'
	)
}

function toErrorWithMessage(maybeError: unknown): ErrorWithMessage {
	if (isErrorWithMessage(maybeError)) return maybeError

	try {
		return new Error(JSON.stringify(maybeError))
	} catch {
		// fallback in case there's an error stringifying the maybeError
		// like with circular references for example.
		return new Error(String(maybeError))
	}
}

function getErrorMessage(error: unknown) {
	return toErrorWithMessage(error).message
}

Conclusion

Don't wave off a compiler error on the grounds that the faulty path seems unreachable. TypeScript is usually right that the unexpected can happen, and the cases it forces you to handle are not as unlikely as they look.