I think what you're really trying to do here is implement a user-defined type guard. Essentially you write a boolean function with the return type arg is SomeType and that tells typescript that if the function is true than arg must be of type Sometype. And if it's false then that means that arg cannot be of type Sometype.
Here is what that function looks like in your case:
function isKind<T extends Kinds>(arg: FooBar, kind: T): arg is Extract<FooBar, {kind: T}> {
return arg.kind === kind;
}
When you use this function inside an if statement, you get meaningful information about the type:
function testUnknown( either: FooBar ) {
if ( isKind(either, "bar") ) {
// we now know that either is Bar
const barVal = either.bar; // ok
const fooVal = either.foo; // error
}
else {
// we now know that either is Foo
const barVal = either.bar; // error
const fooVal = either.foo; // ok
}
}
I personally would prefer to write isKind as a double arrow function so that you could more easily call isKind("foo") inside an array.map or other callback, but that's just down to code style and personal preference.
const isKind = <T extends Kinds>(kind: T) =>
(arg: FooBar): arg is Extract<FooBar, {kind: T}> => {
return arg.kind === kind;
}
Your original foobar function can use your isKind type guard internally. This is similar to @mbdavis's answer but you don't need to assert the type of arg using "as" because typescript already understands that the type has been refined.
function foobar<T extends Kinds>(arg: FooBar, kind: T): Extract<FooBar, {kind: T}> | undefined {
return isKind(arg, kind) ? arg : undefined;
}
But I suspect that you don't really even need this foobar function and can just call isKind directly in your code.
Typescript Playground Link