As of version 6.0, you can use SCAN 0 TYPE set. See https://redis.io/commands/scan#the-type-option
For versions prior to 6.0, you can use a Lua script to filter server-side, saving Round Trip Time (RTT) of doing TYPE calls back from the client.
EVAL "local result = redis.call('SCAN', ARGV[1], 'MATCH', ARGV[2]) local filtered = {} for _,key in ipairs(result[2]) do if redis.call('TYPE', key).ok == ARGV[3] then table.insert(filtered, key) end end result[2] = filtered return result" 0 0 * set
The parameters to the script are 0(numkeys) cursor matchPattern type. E.g. 0 0 * set
Here a friendly view of the Lua script:
local result = redis.call('SCAN', ARGV[1], 'MATCH', ARGV[2])
local filtered = {}
for _,key in ipairs(result[2]) do
    if redis.call('TYPE', key).ok == ARGV[3] then
        table.insert(filtered, key)
    end
end
result[2] = filtered
return result
The returned value is the same as with SCAN, just with the list of keys filtered by type.
As with SCAN, you need to call multiple times until the cursor returned is zero.
This approach is much better than using KEYS, as it won't block the server for a long time.
Next is the same script but with COUNT option, to do less or more work per call. The count should be greater than zero.
EVAL "local result = redis.call('SCAN', ARGV[1], 'MATCH', ARGV[2], 'COUNT', ARGV[3]) local filtered = {} for _,key in ipairs(result[2]) do if redis.call('TYPE', key).ok == ARGV[4] then table.insert(filtered, key) end end result[2] = filtered return result" 0 0 * 100 set
The parameters to the script are 0(numkeys) cursor matchPattern count type. E.g. 0 0 * 100 set