ARTICLE AD BOX
As you are aware, your PossibleTypes isn't a good discriminated union because the intended discriminant property code doesn't discriminate between any of the known variants and the Common base. You want to say that PossibleTypes is A | B | UnknownType where UnknownType's code property is not any of the others.
But TypeScript doesn't currently have negated types as requested in microsoft/TypeScript#4196. A negated type would look like not T where T is a type. There was an implementation at microsoft/TypeScript#29317 but it never made it into the language.
(You can't get it by writing Exclude<string, "A" | "B"> with the Exclude utility type, since Exclude is just a type alias for a distributive conditional type that filters unions, and string isn't a union. Exclude<string, ⋯> is only ever going to result in string or maybe never.)
If negated types did exist then you could write
type KnownTypes = A | B; // this part is NOT valid TypeScript, don't try to use it type UnknownCode = string & not KnownTypes["code"]; interface UnknownType extends Common { code: UnknownCode } type PossibleTypes = UnknownType | KnownTypes;And so then UnknownCode would look like string & not("A" | "B"), meaning any string except "A" or "B". And then PossibleTypes would be a truly discriminated union and everything would work how you want.
But negated types don't exist so you can't do this.
There are various workarounds. One that has been mentioned a bunch is to write a user-defined type guard function to pretend that PossibleTypes is a discriminated union by filtering out Common from it. I'd be a little wary of this since the type A | B | Common might sometimes be reduced to just Common (since Common is a supertype of both A and B). But it could meet your needs.
The workaround I'd like to mention depends on the fact that your objects "come from an external service". Presumably that means you don't actually need TypeScript to know or care or check whether a given string literal is a valid code. There's the KnownTypes, and then there's UnknownType, and all you want to to is check if something is UnknownType, not try to create a new one. So it might as well be true that the actual list of possible codes is the known ones like "A" and "B", plus a single one like, say, "__unknownCode__". Sure, that's not likely to be true, but you're not going to write any code that depends on the difference between that and the actual things. So you could just write
type UnknownCode = "__unknownCode__";and your code would work. The only thing is that you don't want TypeScript IntelliSense to let user's think that the string "__unknownCode__" is useful. Really you don't want a particular string literal, but a nominal type that is a subtype of string but not anything you can easily create inside your code. TypeScript doesn't support true nominal typing, but you can get close, using branded types as described in the TypeScript FAQ. The idea is to say that UnknownCode is both a string literal and something with an extra property that strings don't really have. Like, say,
type UnknownCode = "__unknownCode__" & { __brand: true }That extra property is the "tag" or the "brand". It's like we've put a label on a particular instance of "__unknownCode__" saying that this one has come from our external service. Once you do this, IntelliSense will probably not suggest using the string literal since it's not enough to match the type. The point of branded types is to give you something that TypeScript thinks is distinguishable from the unbranded version, even though at runtime it wouldn't be.
Okay, so let's make that change to the above code:
type KnownTypes = A | B; type UnknownCode = "__unknownCode__" & { __brand: true }; interface UnknownType extends Common { code: UnknownCode } type PossibleTypes = UnknownType | KnownTypes;And now PossibleTypes is indeed a discriminated union and we can use it as you expect:
function f(obj: PossibleTypes): void { if (obj.code === 'B') { console.log(obj.fieldB); // okay } else if (obj.code === 'A') { console.log(obj.fieldA); // okay } else { obj; //^? (parameter) obj: UnknownType } }