As far as I know, there are three situations where the type of an object is looked up at runtime: with dynamic_cast, binding exception objects to handlers and looking up which virtual function to call.
Here's an implementation of using exception handlers to store type information at runtime: https://godbolt.org/z/Mf71bbr7P
#include <type_traits>
#include <typeinfo>
namespace detail {
    template<typename T>
    struct CType;
}
struct Type {
    const std::type_info& type_info;
    template<typename T>
    friend struct detail::CType;
    template<typename T>
    friend bool IsOrInheritsFrom(const T&, const Type& type) noexcept {
        return type.can_catch_thrown_pointer([]{ throw static_cast<T*>(nullptr); });
    }
    Type(const Type&) = delete;
    Type& operator=(const Type&) = delete;
private:
    constexpr Type(const std::type_info& ti) noexcept : type_info(ti) {}
    ~Type() = default;
    virtual bool can_catch_thrown_pointer(void thrower()) const noexcept = 0;
};
namespace detail {
    template<typename T>
    struct CType : Type {
        static const CType instance;
    private:
        constexpr CType() noexcept : Type(typeid(T)) {}
        ~CType() = default;
        bool can_catch_thrown_pointer(void thrower()) const noexcept override {
            try {
                thrower();
            } catch (T*) {
                return true;
            } catch (...) {}
            return false;
        }
    };
    template<typename T>
    constexpr CType<T> CType<T>::instance{}; 
}
template<typename T>
constexpr const Type& TypeOf = detail::CType<typename std::remove_cv<typename std::remove_reference<T>::type>::type>::instance;
#include <vector>
#include <iostream>
struct Dog {};
struct Cat {};
struct Hamster {};
struct Labrador : Dog {};
int main() {
    std::vector<const Type*> validTypes;
    validTypes.push_back(&TypeOf<Dog>);
    validTypes.push_back(&TypeOf<Cat>);
    validTypes.push_back(&TypeOf<Hamster>);
    Labrador* somePet = new Labrador();
    for (const Type* t : validTypes) {
        if (IsOrInheritsFrom(*somePet, *t)) {
            std::cout << "Inherits from " << t->type_info.name() << '\n';
        } else {
            std::cout << "Does not inherit from " << t->type_info.name() << '\n';
        }
    }
}
Which throws (Labrador*) nullptr and each type in the vector sees if it can catch it. And it can be caught as a Dog*.
You can do something similar with dynamic_cast. The only way I can think of doing it is having a common base class between all types you want to be able to check, then each runtime type can try to dynamic_cast: https://godbolt.org/z/s9oearhGP
#include <type_traits>
#include <typeinfo>
#include <memory>
struct RuntimeTypeCheckable {
protected:
    RuntimeTypeCheckable() = default;
    RuntimeTypeCheckable(const RuntimeTypeCheckable&) = default;
    RuntimeTypeCheckable& operator=(const RuntimeTypeCheckable&) = default;
    virtual ~RuntimeTypeCheckable() = default;
};
namespace detail {
    template<typename T>
    struct CType;
}
struct Type {
    const std::type_info& type_info;
    template<typename T>
    friend struct detail::CType;
    template<typename T>
    friend bool IsOrInheritsFrom(const T& object, const Type& type) noexcept {
        return type.can_dynamic_cast_pointer(std::addressof(object));
    }
    Type(const Type&) = delete;
    Type& operator=(const Type&) = delete;
private:
    constexpr Type(const std::type_info& ti) noexcept : type_info(ti) {}
    ~Type() = default;
    virtual bool can_dynamic_cast_pointer(const RuntimeTypeCheckable* ptr) const noexcept = 0;
};
namespace detail {
    template<typename T>
    struct CType : Type {
        static const CType instance;
    private:
        constexpr CType() noexcept : Type(typeid(T)) {}
        ~CType() = default;
        bool can_dynamic_cast_pointer(const RuntimeTypeCheckable* ptr) const noexcept override {
            return dynamic_cast<const T*>(ptr) != nullptr;
        }
    };
    template<typename T>
    constexpr CType<T> CType<T>::instance{}; 
}
template<typename T>
constexpr const Type& TypeOf = detail::CType<typename std::remove_cv<typename std::remove_reference<T>::type>::type>::instance;
#include <vector>
#include <iostream>
struct Dog : virtual RuntimeTypeCheckable {};
struct Cat : virtual RuntimeTypeCheckable {};
struct Hamster : virtual RuntimeTypeCheckable {};
struct Labrador : Dog {};
int main() {
    std::vector<const Type*> validTypes;
    validTypes.push_back(&TypeOf<Dog>);
    validTypes.push_back(&TypeOf<Cat>);
    validTypes.push_back(&TypeOf<Hamster>);
    Labrador* somePet = new Labrador();
    for (const Type* t : validTypes) {
        if (IsOrInheritsFrom(*somePet, *t)) {
            std::cout << "Inherits from " << t->type_info.name() << '\n';
        } else {
            std::cout << "Does not inherit from " << t->type_info.name() << '\n';
        }
    }
}
Though this requires the types to have RuntimeTypeCheckable as a base class.
As for virtual functions, you would need something like this: https://godbolt.org/z/jq6b8E7bx
#include <typeinfo>
struct RuntimeTypeCheckable {
public:
    virtual bool is_or_inherits_from(const std::type_info& type_info) const noexcept {
        return type_info == typeid(RuntimeTypeCheckable);
    }
    friend bool IsOrInheritsFrom(const RuntimeTypeCheckable& object, const std::type_info& type) noexcept {
        return object.is_or_inherits_from(type);
    }
protected:
    RuntimeTypeCheckable() = default;
    RuntimeTypeCheckable(const RuntimeTypeCheckable&) = default;
    RuntimeTypeCheckable& operator=(const RuntimeTypeCheckable&) = default;
    virtual ~RuntimeTypeCheckable() = default;
};
#include <typeindex>
#include <vector>
#include <iostream>
struct Dog : virtual RuntimeTypeCheckable {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Dog) || RuntimeTypeCheckable::is_or_inherits_from(type_info);
    }
};
struct Cat : virtual RuntimeTypeCheckable {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Dog) || RuntimeTypeCheckable::is_or_inherits_from(type_info);
    }
};
struct Hamster : virtual RuntimeTypeCheckable {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Dog) || RuntimeTypeCheckable::is_or_inherits_from(type_info);
    }
};
struct Labrador : Dog {
    bool is_or_inherits_from(const std::type_info& type_info) const noexcept override {
        return type_info == typeid(Labrador) || Dog::is_or_inherits_from(type_info);
    }
};
int main() {
    std::vector<const std::type_info*> validTypes;
    validTypes.push_back(&typeid(Dog));
    validTypes.push_back(&typeid(Cat));
    validTypes.push_back(&typeid(Hamster));
    Labrador* somePet = new Labrador();
    for (const std::type_info* t : validTypes) {
        if (IsOrInheritsFrom(*somePet, *t)) {
            std::cout << "Inherits from " << t->name() << '\n';
        } else {
            std::cout << "Does not inherit from " << t->name() << '\n';
        }
    }
}
Which requires the most modifications to the types being used, but is the most readable. The boilerplate can be reduced with CRTP.