Suppose I have this union type:
type Foo = {tag: 'A', name: string} | {tag: 'B', value: number}
I am trying to write a function that gets a tag value ('A' | 'B'
) and then does some business-specific lookup in a list of Foo
s and returns a matching element. For the purpose of this question we can think of this very trivial function:
function findFirst(foos: Foo[], tag: 'A' | 'B') {
for (const foo of foos) {
if (foo.tag === tag) {
return foo
}
}
return undefined
}
Suppose I now call this function passing 'A' as the second argument. It is clear that it either returns undefined
or a {tag: 'A', name: string}
object. It just cannot return a {tag: 'B', value: number}
in this situation.
Still, the TSC does not seem to be able to infer this. When writing an expression such as:
findFirst([{tag: 'A', name: 'X'}], 'A')?.name
The TSC errors with Property 'name' does not exist on type 'Foo'.
.
I am guessing that I need to help the compiler infer the right by expliticly defining the return type of findFirst()
but I am not sure how.
CodePudding user response:
You need to add a type parameter to the function to capture the actual type of tag being passed in. You then need to filter the Foo
union using the Extract
predefined conditional type:
function findFirst<T extends Foo['tag']>(foos: Foo[], tag: T) {
for (const foo of foos) {
if (foo.tag === tag) {
return foo as Extract<Foo, { tag: T }>
}
}
return undefined
}
CodePudding user response:
You can use function overloads to declare the result without having to use cast.
type FooA = {tag: 'A', name: string};
type FooB = {tag: 'B', value: number};
type Foo = FooA | FooB
function findFirst(foos: Foo[], tag: 'A'): FooA | undefined;
function findFirst(foos: Foo[], tag: 'B'): FooB | undefined;
function findFirst(foos: Foo[], tag: 'A' | 'B') {
for (const foo of foos) {
if (foo.tag === tag) {
return foo
}
}
return undefined
}
findFirst([{tag: 'A', name: 'X'}], 'A')?.name
findFirst([{tag: 'A', name: 'X'}], 'B')?.value
// TS Error as expected
findFirst([{tag: 'A', name: 'X'}], 'A')?.value