For a more general solution that works in all cases, check out this trick:
type Untag<T, Tag> =
((_: FlipVariance<T>) => never) extends (
(_: FlipVariance<infer T & Tag>) => infer T
) ?
T
: never;
interface FlipVariance<T> {
(_: T): void;
}
type TestResult = Untag<string & { _tag: "foo" }, { _tag: "foo" }>; // => string
This is made possible by TypeScript’s special support for intersection type inference in generics, allowing functions like untag<T>(a: T & { tag: any }): T
to work.