Jason Lee
Jason Lee

Reputation: 377

Why can't typescript maintain types in Array.prototype.filter()?

The following code is fine but if you uncomment the last line ts will give an error. Seems like .filter() throws away the already inferred type from the previous step which doesn't make sense to me. Please explain how ts works here and suggest a solution.

type ItemIds =
    | 'foo'
    | 'bar';

interface Item {
    id: ItemIds;
};

const items: Item[] = [
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
]
//.filter((item) => true)

https://www.typescriptlang.org/play?#code/C4TwDgpgBAksEFsYBMDOUC8AoKuoB8oByAMwHsyic9CiAjAQwCciBuLLASwDt4mSGAY2hxEUAN7VcnZAC5Y8JGnYBfdlkFluqYFE6LU80QgDaAXUxQTUiTbx65xcpQA0NlW-uT79mfPrMRJ54KlhmWAD0EVAAdCScADZ8ABTJ+ogAlJgAfFDATACuEBlAA

Upvotes: 2

Views: 131

Answers (4)

Balázs Édes
Balázs Édes

Reputation: 13807

It would maintain static typing if you were to declare your starting array as Item[], eg.:

const items: Item[] = [
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
]

const filteredItems: Item[] = items.filter(item => true)

The issue is with the string union type ItemIds, as it's interpreted as string without you telling the compiler otherwise.

The same issue comes up if you just let the compiler infer the type of items first, then reassign it:

// This wont compile either.
const items = [
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
]

const typedItems: Item[] = items

Upvotes: 2

Archimedes Trajano
Archimedes Trajano

Reputation: 41240

It's because

[ { id: 'foo' }, { id: 'bar' }] 

is still a { id: string } [] by the time you filter.

There's two approaches to this:

  1. process after
const items: Item[] = [
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
]
const filtered = items.filter((item) => true)
  1. Cast before
const items: Item[] = ([
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
] as Item[])
.filter((item) => true)

With the second approach you don't need to explicitly state the type since it will be implied already

const items = ([
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
] as Item[])
.filter((item) => true)

Upvotes: 1

T.J. Crowder
T.J. Crowder

Reputation: 1074148

When you assign the literal array to items, even though the type of the literal is {id: string;}[] (not [id: "foo" | "bar";]{} aka Item[]), TypeScript can see that all the id values fit into "foo" | "bar" so it allows the assignment to occur.

However, if you call .filter on that array, it can't do that anymore. The type of the array is {id: string;}[], so that's the result of filter. It can't know that filter doesn't modify id values, it just knows that the types for filter say it will return an array of the same type. Which is {id: string;}[], not {id: "foo" | "bar";}[] (Item[]).

If you ensure that the array you're calling filter on has the type Item[], it will work:

const items = ([
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
] as Item[])
.filter((item) => true);

Playground link

Upvotes: 3

Lev
Lev

Reputation: 15674

You have to declare items first

interface Item {
    id: ItemIds;
};

const items: Item[] = [
    {
        id: 'foo',
    },
    {
        id: 'bar',
    }
]
const filtered = items.filter((item) => true) // filtered has type Item[]

Upvotes: 0

Related Questions