Skip to content

pluck($value, $key)->all() returns array<array-key, mixed> instead of array<KeyType, ValueType> from @property annotations #967

@alies-dev

Description

@alies-dev

Summary

When Collection::pluck($valueColumn, $keyColumn) is called on an Eloquent collection with both arguments, and both columns have known types via @property annotations on the model, the resulting ->all() is still inferred as array<array-key, mixed> instead of array<KeyType, ValueType>. This forces a LessSpecificReturnStatement on any method that declares the narrowed shape as its return type.

This is the two-argument sibling of #486 (which covered single-argument pluck('column') and is closed completed). Two-argument pluck is not narrowed today.

Repro

use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;

/**
 * @property string $title
 * @property string $slug
 */
class Article extends Model {}

/**
 * @property string $name
 *
 * @method HasMany<Article, $this> articles()
 */
class Author extends Model
{
    public function articles(): HasMany
    {
        return $this->hasMany(Article::class);
    }
}

class Presenter
{
    /**
     * @return array<string, string>
     */
    public function titlesBySlug(Author $author): array
    {
        return $author->articles()->pluck('title', 'slug')->all();
    }
}

Psalm reports:

ERROR: LessSpecificReturnStatement
The type 'array<array-key, mixed>' is more general than the declared return type
'array<string, string>' for Presenter::titlesBySlug (see https://psalm.dev/129)
    return $author->articles()->pluck('title', 'slug')->all();

Same shape applies when the collection comes from Model::query()->get()->pluck('value_col', 'key_col'), or from any Eloquent\Collection whose model declares both columns via @property.

Expected

For Collection<TKey, TModel of Model>::pluck(string \$value, string \$key):

  • if TModel has @property V \$value and @property K \$key (where K resolves to an array-key type), the return should be Support\Collection<K, V>;
  • consequently ->all() should resolve to array<K, V>.

Today the plugin appears to leave both TKey and TValue unresolved, so the call chain collapses to array<array-key, mixed>.

Environment

  • psalm/plugin-laravel: dev-master
  • vimeo/psalm: 7.0.0-beta19
  • PHP: 8.4

Workaround

/** @var array<string, string> $titlesBySlug */
$titlesBySlug = $author->articles()->pluck('title', 'slug')->all();
return $titlesBySlug;

…or splitting into two pluck calls and recombining, which is strictly worse.

Related

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions