If you are a TypeScript user, you may have experienced the following scenario:
interface Base {
type: 't1' | 't2'
}
interface Extend1 extends Base {
ccc: string
bbb: number
}
interface Extend2 extends Base {
aaa: boolean
}
You have an array where only some fields of the type are determined, and you need to operate on each element during traversal. The actual type of the element can only be determined through some custom logic.
When you want to get the result with just one call and also want to filter out elements of a certain subtype, you often write code like this:
const arr: Base[] = [/* ... */]
const arrOfExt1 = arr
.filter(item => item.type === 't1')
.map(item => item as Extend1)
The use of as
in the above .map
looks ugly and redundant because it seems to be doing nothing meaningful.
The core issue is that the return value of .filter
seems to still be just Base[]
. But when you open the TS built-in type definition of the .filter
method:
Oh, there's another option? It seems that you can pass a type parameter to the .filter
method or explicitly define a type predicate in the signature of the argument function passed to .filter
. TypeScript's "type predicate" can be used to customize type assertions, so we can do this:
const arrOfExt1 = arr
.filter((item): item is Extend1 => item.type === 't1')
console.log(arr_of_ext1)
// ?^ Extend1[]
According to the code, the side using .filter
should belong to the business logic, and the predicate function passed in is a separate util. This type assertion function may be reused multiple times, so consider extracting the judgment function:
function isExtend1(item: Base): item is Extend1 {
return item.type === 't1'
}
const arrOfExt1 = arr.filter(isExtend1)
Finally, the result we see is very concise, intuitive, and readable.