# Pre-RFC: non-footgun non-static TypeId

**URL:** <https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079>\
**Category:** Uncategorized\
**Created:** [July 24, 2022, 8:17pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079 "2022-07-24T20:17:08Z")\
**Posts on this page:** 17\
**Page:** 1

<div class="post-metadata">

**Author:** ![dtolnay](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/dtolnay/32/1447_2.png) [@dtolnay](https://internals.rust-lang.org/u/dtolnay)\
**Post date:** [July 24, 2022, 8:17pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/1 "2022-07-24T20:17:08Z")

</div>

Proposal for how to unblock use cases that involve TypeId of potentially non-'static types, while exposing zero of the risks that took down the previous attempt at this.

# Background

Type id is currently exposed in the standard library as follows:

```rust
// in core::any⸺
pub struct TypeId {…}

impl TypeId {
    pub fn of<T>() -> Self
    where
        T: 'static + ?Sized;
}

impl Copy, Clone, Eq, Ord, Debug, Hash

// in core::intrinsics⸺
#[unstable]
pub extern "rust-intrinsic" fn type_id<T>() -> u64
where
    T: 'static + ?Sized;

```

[RFC 1849](https://github.com/rust-lang/rfcs/pull/1849) proposed deleting the `T: 'static` bound from the above. This RFC was **accepted** by the lang team in 2017, and then **unaccepted** in 2020 in [#41875](https://github.com/rust-lang/rust/issues/41875) without ever having been implemented, attributed to _"potential for confusion and misuse"_.

The concern is that since lifetimes are erased at runtime in Rust, `&'a U` and `&'b U` necessarily have the same `TypeId` regardless of lifetimes. Thus pretty much any use of `TypeId` with non-'static types where downcasting is involved is going to be unsound.

# Counterproposal

My pre-RFC proposes the following API:

```rust
// in core::any⸺
pub struct TypeId {…}

impl TypeId {
    pub fn of<T>() -> Self
    where
        T: 'static + ?Sized,
    {
        TypeId(intrinsics::type_id::<T>())
    }

    pub fn same_as<T>(&self) -> bool
    where
        T: ?Sized,
    {
        intrinsics::all_types_with_this_id_are_static::<T>()
            && self.0 == intrinsics::type_id::<T>()
    }
}

// in core::intrinsics⸺
#[unstable]
pub extern "rust-intrinsic" fn type_id<T>() -> u64
where
    T: ?Sized;

#[unstable]
pub extern "rust-intrinsic" fn all_types_with_this_id_are_static<T>() -> bool
where
    T: ?Sized;

```

## But is this useful? I still see a `'static` bound…

Yes! This API would be _ **amazing** _ for Serde.

In Serde, data formats contain code that is generic over what type is being deserialized or serialized, and those generics usually do not have a 'static bound because plenty of non-'static types can be deserialized and serialized.

However, data formats often want to implement special behavior for a _small_ set of special types unique to that data format, for example DateTime in TOML.

```rust
fn part_of_deserializer<'de, V>(data: D, visitor: V) -> Result<V::Value>
where
    V: serde::de::Visitor<'de>,
{
    if /** V == DateTimeVisitor<Value = DateTime> */ {
        let datetime: DateTime = deserialize_datetime(data)?;
        Ok(unsafe {
            mem::transmute_copy(&ManuallyDrop::new(datetime))
        })
    } else {
        deserialize_anything_else(data, visitor)
    }
}

```

This behavior is impossible to express today. With the API from the pre-RFC, the condition becomes implementable as:

```rust
    if TypeId::of::<DateTimeVisitor>().same_as::<V>() {

```

## But is this risky to expose?

No! You still cannot get a `TypeId` for a type that is not `'static`!

You can only get a `TypeId` of a type that is statically `'static`, and then check whether some other type (which may or may not be `'static`) is the same as the first one **and** has the same `'static` lifetime.

# Examples

```rust
struct StaticStr(&'static str);

struct BorrowedStr<'a>(&'a str);

// true
assert!(TypeId::of::<StaticStr>().same_as::<StaticStr>());

// false because different type lol
assert!(! TypeId::of::<StaticStr>().same_as::<&'static str>());

// false because all_types_with_this_id_are_static is false
assert!(! TypeId::of::<BorrowedStr<'static>>().same_as::<BorrowedStr<'a>>());

```

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [July 24, 2022, 8:26pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/2 "2022-07-24T20:26:02Z")

</div>

I haven’t thought too deeply about how much of a problem this is, but a potential remaining concern with the API you’re proposing is that a type like

```rust
struct Foo(&'static str);

```

could be changed into

```rust
struct Foo<T = &'static str>(T);

```

with the intention of this being a non-breaking change, however this changes the behavior of

```rust
intrinsics::all_types_with_this_id_are_static::<Foo>()

```

from `true` to `false`, and thus breaks use-cases of `type_id.same_as::<Foo>()`.

---

<div class="post-metadata">

**Author:** ![eddyb](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/eddyb/32/8411_2.png) [@eddyb](https://internals.rust-lang.org/u/eddyb)\
**Post date:** [July 24, 2022, 8:52pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/3 "2022-07-24T20:52:50Z")

</div>

Just driving by to say that [I've been exposed to something like this in the wild](https://github.com/sagebind/castaway/pull/6#issuecomment-1151011368) (though they had done it unsoundly abusing monomorphization behavior that we'll hopefully close up to some extent).

It was frankly confusing as framed in their system, but the trick is that a type with no "lifetime positions" (in its monomorphic fully-normalized form that is a tree of type constructor applications) can be known to meet a `: 'static` _without_ having to specify that bound statically (so a bit like `needs_drop`).

In a sense, this can be seen as a version of `T: 'static` that can be used during trait impl specialization, in that it only matches when the bound holds _across all possible choices of lifetimes in `T`_ (which gets back to "`T` has no lifetime positions" almost by definition, since any of them would invalidate `'static`).

* * *

I agree about integrating with `TypeId` (and `Any`, in their case), but I'm not fond on having a separate `all_types_with_this_id_are_static` intrinsic (my suggestion was `TypeId::of_lifetimeless`, which would have its own intrinsic returning `Option`).

Also, the problem with _not_ labeling it specially ("lifetimeless" was my bikeshed) is that it can fail for types that are in fact `: 'static`.

To complete your example:

```rust
assert_eq!(TypeId::of::<&'static str>(), TypeId::of::<&'static str>());
assert!(! TypeId::of::<&'static str>().same_as::<&'static str>());

```

So by that measure, `same_as` is too misleading of a name.

Whereas if we added suffixed methods to `TypeId`/`Any`, this would make sense IMO:

```rust
let any_str = &"foo" as &dyn Any;

// Successes:
assert_eq!(any_str.type_id(), TypeId::of::<&'static str>());
assert_eq!(any_str.downcast_ref::<&'static str>(), Some("foo"));

// Failures:
assert_eq!(TypeId::of_lifetimeless::<&'static str>(), None);
assert_eq!(any_str.downcast_lifetimeless_ref::<&'static str>(), None);

```

Maybe "lifetimeless" is a poor name for the concept, but regardless we should come up with one that's distinguishing enough to not be mistaken as being a helpful shorthand, and thoroughly document it.

---

<div class="post-metadata">

**Author:** ![carbotaniuman](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/carbotaniuman/32/6981_2.png) [@carbotaniuman](https://internals.rust-lang.org/u/carbotaniuman)\
**Post date:** [July 25, 2022, 3:32pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/4 "2022-07-25T15:32:42Z")

</div>

Isn't such a change already breaking due to the large amount of inference breakages associated? Iirc yu can't really generify a struct due to a lack of inference for generic defaults.

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [July 25, 2022, 3:42pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/5 "2022-07-25T15:42:25Z")

</div>

Well, the standard library is doing it with `Box` and `Vec`, adding a allocator parameter, so I guess it _is_ possible.

---

<div class="post-metadata">

**Author:** ![CAD97](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cad97/32/3460_2.png) [@CAD97](https://internals.rust-lang.org/u/CAD97)\
**Post date:** [July 25, 2022, 4:49pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/6 "2022-07-25T16:49:54Z")

</div>

It's worse for functions, because `foo` means `foo::<{?0}>` with an inference variable. For structs, it's significantly less breaking, since `Foo` means `Foo::<{default}>`.

If all implementations existing before the generalization always apply to the generalized form, then this cannot break inference.

If any implementations existing before the generalization are limited to being provided solely for the prior type, using that implementation will at least some of the time allow inference to resolve to the single option.

If any implementations existing before the generalization are provided generically but do not apply to the whole generalization, then inference breakage is possible.

The biggest hurdle is not generalizing the struct itself; it's generalizing the functions which mention the new generics when not covered by the generalized `Self`. So perhaps the choice of a tuple struct was not the best choice here, since it does replace `fn Foo(&'static str) -> &'static str` with `for<T> fn Foo(T) -> T`; doing the generalization for a braced struct is generally considered to be minor/allowed inference breakage. (Again, generalizing the `impl`s requires separate justification of being non-breaking.)

---

<div class="post-metadata">

**Author:** ![steffahn](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/steffahn/32/13288_2.png) [@steffahn](https://internals.rust-lang.org/u/steffahn)\
**Post date:** [July 25, 2022, 5:39pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/7 "2022-07-25T17:39:30Z")

</div>

> [@CAD97](#):
>
> since it does replace `fn Foo(&'static str) -> &'static str` with `for<T> fn Foo(T) -> T`

Only if the field is public. Admitted, I didn't mark the struct public either, so the field being private was somewhat implicit.

---

<div class="post-metadata">

**Author:** ![197g](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/197g/32/7276_2.png) [@197g](https://internals.rust-lang.org/u/197g)\
**Post date:** [July 25, 2022, 5:42pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/8 "2022-07-25T17:42:30Z")

</div>

There's already a use case for `all_types_with_this_id_are_static`, motivated entirely differently but with the same outcome: We can't make `fn(&'a())` have a `'static` lifetime instead of `'a` at the moment, even though of course there's nothing preventing such methods from existing outside scopes with `'a` lifetime bounds. Otherwise any `fn(&'b())` could be safely cast to `fn(&'a())` via `Any`, trivially unsound. This generalizes to other type construction that is contravariant in lifetimes for any other reason including some `Box<dyn Trait<'a>>` use cases.

---

<div class="post-metadata">

**Author:** ![semicoleon](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/semicoleon/32/7397_2.png) [@semicoleon](https://internals.rust-lang.org/u/semicoleon)\
**Post date:** [August 2, 2022, 7:26pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/9 "2022-08-02T19:26:59Z")

</div>

It doesn't actually matter whether all types with the ID are static does it? It seems like if we're adding an intrinsic anyway it would make more sense to add a purpose built intrinsic (especially since as far as I can tell the rejected RFC purely changed the definition of the `type_id` intrinsic and not the bounds on `TypeId`'s associated functions)

```rust
pub extern "rust-intrinsic" fn type_id_eq_ignoring_lifetimes<T>(type_id: TypeId) -> u64
where
    T: ?Sized;

```

Then instead of `same_as` we could do something like

```rust
impl TypeId {
    // eq is probably still a little confusing here
    pub fn eq_ignoring_lifetimes<T>(&self) -> bool
    where
        T: ?Sized,
    {
        intrinsics::type_id_eq_ignoring_lifetimes::<T>(self.0)
    }
{

```

I think that would avoid at least the point of contention that appears to have gotten the previous RFC unaccepted. Though I'm still kind of unclear about why the 'static bound is important on the intrinsic since non-nightly users can't access it except through `TypeId` anyway.

---

<div class="post-metadata">

**Author:** ![eddyb](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/eddyb/32/8411_2.png) [@eddyb](https://internals.rust-lang.org/u/eddyb)\
**Post date:** [August 3, 2022, 2:10pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/10 "2022-08-03T14:10:29Z")

</div>

> [@semicoleon](#):
>
> It doesn't actually matter whether all types with the ID are static does it?

I believe it does, in that it's the only way I know of in which such a feature has sound usecases.

IIUC your `eq_ignoring_lifetimes` means this is unsound:

```rust
// R => "runtime", S => "static"
fn try_cast<R, S: 'static>(r: R) -> Result<S, R> {
    if TypeId::of::<S>().eq_ignoring_lifetimes::<R>() {
        Ok(transmute(r)) // realistically, transmute_copy + forget
    } else {
        Err(r)
    }
}

```

If I do e.g. `try_cast::<_, &'static str>(&"foo".to_string())` the above would return `Ok("foo")`, except pointing to the heap, so when the `String` is deallocated, you'll pretty much be guaranteed UB unless you get rid of/never access again the bad `&'static str` first.

But if it's limited to "all types with the ID are `'static`" , then you would get `Err` because `&'a str` is only `'static` when `'a` is, not always (and AFAIK you can do casts like thing _as long as_ you can guarantee that the types contain no lifetime positions, which is currently isomorphic to "always `'static`").

This is the kind of usecase I was referencing [in my earlier comment above](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/3), from which I linked [https://github.com/sagebind/castaway/pull/6#issuecomment-1151011368](https://github.com/sagebind/castaway/pull/6#issuecomment-1151011368).

---

<div class="post-metadata">

**Author:** ![cfrantz](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/cfrantz/32/9821_2.png) [@cfrantz](https://internals.rust-lang.org/u/cfrantz)\
**Post date:** [September 20, 2022, 8:07pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/11 "2022-09-20T20:07:03Z")

</div>

As an [amateur type-caster](https://github.com/cfrantz/serde-annotate/blob/main/src/annotate.rs#L87-L100) myself, I'd really like to be able to deal with non-`'static` types in a sane way.

I'd prefer to see something like:

```rust
enum TypeId {
    // Type is 'static, safe to use for casting.
    Static(u64),
    // Type is not 'static, don't point this gun at your foot.
    Unsafe(u64),
}

impl Clone, Copy, etc.
impl Eq such that TypeId::Unsafe(x) never compares equal to anything, even itself.

```

I _think_ this gives dtolnay's `same_as<T>(&self)` in the comparison between two `TypeId`s: non-static types can't be determined to be the same type. Does this venture too close to what #41875 was trying to avoid?

Can someone point me to some reading about the unsoundness of downcasting non-'static types? (I think I understand, but I'd like to make sure).

---

<div class="post-metadata">

**Author:** ![Jules-Bertholet](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/jules-bertholet/32/10671_2.png) [@Jules-Bertholet](https://internals.rust-lang.org/u/Jules-Bertholet)\
**Post date:** [September 20, 2022, 11:51pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/12 "2022-09-20T23:51:29Z")

</div>

Currently, Rust's only form of subtyping is via lifetimes, so "all types with this `TypeId` are `'static`" ⇒ "all types with this `TypeId` can be freely transmuted to one another". However, new forms of subtyping might be added in the future, breaking this assumption.

---

<div class="post-metadata">

**Author:** ![LegionMammal978](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/legionmammal978/32/8985_2.png) [@LegionMammal978](https://internals.rust-lang.org/u/LegionMammal978)\
**Post date:** [September 21, 2022, 2:02am UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/13 "2022-09-21T02:02:57Z")

</div>

> Currently, Rust's only form of subtyping is via lifetimes

This is incorrect; subtyping also extends to HRTB instantiations. For instance, `fn(&'static i32) -> &'static i32` is a subtype of `for<'a> fn(&'a i32) -> &'a i32`, and both types are `'static`. However, the two types have different `TypeId`s, so the implication still holds.

---

<div class="post-metadata">

**Author:** ![Jules-Bertholet](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/jules-bertholet/32/10671_2.png) [@Jules-Bertholet](https://internals.rust-lang.org/u/Jules-Bertholet)\
**Post date:** [September 21, 2022, 2:32am UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/14 "2022-09-21T02:32:04Z")

</div>

The difficulty here (which applies both to lifetime HRTB and potential new forms of subtyping) is that switching a return type, associated const, etc from a supertype or a subtype should ideally be a non-breaking change. But having different `TypeId`s for subtypes makes this a breaking change, and making the subtypes have the same `TypeId` is unsound.

---

<div class="post-metadata">

**Author:** ![bjorn3](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/bjorn3/32/2736_2.png) [@bjorn3](https://internals.rust-lang.org/u/bjorn3)\
**Post date:** [September 21, 2022, 12:24pm UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/15 "2022-09-21T12:24:20Z")

</div>

Another issue is that by the time the type\_id intrinsic is called, all lifetimes are already erased. This means that it is impossible to give `&'static u8` a different TypeId fron `&'a u8`. It would only be possible to use the `TypeId::Static` variant for types that don't have any lifetime parameters at all.

> [@cfrantz](#):
>
> `impl Eq such that TypeId::Unsafe(x) never compares equal to anything, even itself.`

`Eq` requires the relation to be reflexive, in other words it requires `x == x` to hold for all `x`. This property is why we have `Eq` in the first place instead of just `PartialEq`, which doesn't require this.

---

<div class="post-metadata">

**Author:** ![matt1985](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/matt1985/32/4772_2.png) [@matt1985](https://internals.rust-lang.org/u/matt1985)\
**Post date:** [October 11, 2022, 7:25am UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/16 "2022-10-11T07:25:58Z")

</div>

I just want to mention a (admitedly extremely niche) usecase for getting `TypeId`s of non-static types.

I have a `TypeEq<L, R>` type that is only constructible if its two type arguments are the same type, and allows me to do [a limited form of polymorphism in const fns on stable](https://play.rust-lang.org/?version=stable&mode=release&edition=2021&gist=09f49fffa672f003e92f94c4c193bac5).

I put `TypeEq` in enums where only one variant can be constructed for any given combination of generic arguments, so to help the compiler remove dead code I use a function (`fn reachability_hint(self: TypeEq<L, R>)`) that calls `unreachable_unchecked` when it can prove that the type arguments of `TypeEq<L, R>` aren't the same type.

The problem is that on stable I can only use size and alignment to prove that `L` and `R` are different types, which leaves a lot of opportunities to remove dead code on the table.

Being able to construct and compare `TypeId`s in const contexts for any type would make it possible to remove virtually all dead branches, except for those that only differ by lifetimes, which is fine with me.

---

<div class="post-metadata">

**Author:** ![system](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/system/32/14092_2.png) [@system](https://internals.rust-lang.org/u/system)\
**Post date:** [January 9, 2023, 7:26am UTC](https://internals.rust-lang.org/t/pre-rfc-non-footgun-non-static-typeid/17079/17 "2023-01-09T07:26:08Z")

</div>

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.
