PostGIS ST_Intersection of polygons can return lines
This might be a good spot to use an SQL-language function. Here's a quick one that should work for this situation:
CREATE OR REPLACE FUNCTION PolygonalIntersection(a geometry, b geometry)
RETURNS geometry AS $$
SELECT ST_Collect(geom)
FROM
(SELECT (ST_Dump(ST_Intersection(a, b))).geom
UNION ALL
-- union in an empty polygon so we get an
-- empty geometry instead of NULL if there
-- is are no polygons in the intersection
SELECT ST_GeomFromText('POLYGON EMPTY')) SQ
WHERE ST_GeometryType(geom) = 'ST_Polygon';
$$ LANGUAGE SQL;
This will retain the polygonal components of an intersection, but throw away everything else. It always returns a MultiPolygon, even if you have one or no components.
WITH
square as (SELECT ST_GeomFromText('POLYGON ((0 0, 0 1, 1 1, 1 0, 0 0))') AS geom),
biggersquare as (SELECT ST_GeomFromText('POLYGON ((0 0, 0 10, 10 10, 10 0, 0 0))') AS geom),
adjacentsquare as (SELECT ST_GeomFromText('POLYGON ((0 0, 1 0, 1 -1, -1 -1, 0 0))') AS geom)
SELECT ST_AsText(PolygonalIntersection(square.geom, biggersquare.geom))
FROM square, biggersquare;
--"MULTIPOLYGON(((0 0,0 1,1 1,1 0,0 0)))"
SELECT ST_AsText(PolygonalIntersection(square.geom, adjacentsquare.geom))
FROM square, adjacentsquare;
--"MULTIPOLYGON(EMPTY)"
Very good answer from @dbaston. However, returning an empty geometry instead of null might cause issues because returned empty geometry has no srid. St_Intersection might return MultiPolygon as well. This updated function was really useful for me:
CREATE OR REPLACE FUNCTION PolygonalIntersection(a geometry, b geometry)
RETURNS geometry AS $$
SELECT ST_Collect(geom)
FROM
(SELECT (ST_Dump(ST_Intersection(a, b))).geom
) SQ
WHERE ST_GeometryType(geom) = 'ST_Polygon' OR ST_GeometryType(geom) = 'ST_MultiPolygon';
$$ LANGUAGE SQL;