TL;DR

Khi bạn dùng .filter() hay .find() để lọc mảng lúc runtime, TypeScript compiler không tự động thu hẹp kiểu dữ liệu tương ứng. Dù array lúc thực thi chỉ còn Square[], TypeScript vẫn thấy (Circle | Square | Rectangle)[] và báo lỗi khi bạn truy cập .size. Fix chỉ một dòng: dùng type predicate tường minh.

Vấn đề cụ thể

Đây là đoạn code trông có vẻ đúng nhưng TypeScript sẽ từ chối:

type Shape = Circle | Square | Rectangle;
const shapes: Shape[] = [/* hỗn hợp */];

// TypeScript infer: Shape | undefined
const firstSquare = shapes.find(shape => shape.type === "SQUARE");

// Lỗi compile: Property 'size' does not exist on type 'Square | Rectangle | Circle | undefined'
console.log(firstSquare?.size);

Ở runtime, firstSquare chắc chắn là Square - nhưng TypeScript không biết điều đó. Nó thấy callback trả về boolean, không thể suy ra bạn đang lọc theo tiêu chí gì.

Nguyên nhân gốc rễ: type narrowing không vượt qua ranh giới hàm. Control-flow analysis (CFA) của TypeScript hoạt động rất tốt trong một scope, nhưng không đi xuyên vào function khác để xem logic bên trong.

Giải pháp: Type Predicate

Bạn phải nói cho TypeScript biết kết quả lọc là gì bằng cú pháp parameterName is Type:

// Khai báo type guard tường minh
const isSquare = (shape: Shape): shape is Square => {
  return shape.type === "SQUARE";
};

// Truyền thẳng vào .find() - không wrap thêm arrow function
const firstSquare = shapes.find(isSquare);

// TypeScript infer: Square | undefined
console.log(firstSquare?.size); // OK

Tương tự với .filter():

const onlySquares = shapes.filter(isSquare);
// onlySquares: Square[] - không còn Circle hay Rectangle trong kiểu

Cơ chế hoạt động: Array.find()Array.filter() có overload riêng cho type predicate. Khi bạn truyền hàm có return type value is S, TypeScript kích hoạt overload đó và trả về S[] thay vì T[].

3 gotcha phổ biến

1. Wrap thêm arrow function - type guard mất tác dụng

// Sai: wrapper arrow function không có type predicate
const onlyCircles = shapes.filter(shape => isCircle(shape));
// onlyCircles vẫn là: Shape[]

// Đúng: truyền thẳng
const onlyCircles = shapes.filter(isCircle);
// onlyCircles: Circle[]

2. Type guard không type-safe nội tại

// TypeScript CHẤP NHẬN đoạn này dù logic hoàn toàn sai
function isString(s: any): s is string {
  return typeof s === 'number'; // sai logic nhưng compile OK
}

Compiler tin bạn hoàn toàn. Nếu logic type guard sai, bạn tự tạo ra lỗ hổng type-safety cho chính mình.

3. Dùng as casting che giấu lỗi

// Compile OK, nhưng nguy hiểm
const firstSquare = shapes.find(s => s.type === "SQUARE") as Square;

// Nếu ai đó đổi condition thành "CIRCLE" sau này:
const firstSquare = shapes.find(s => s.type === "CIRCLE") as Square;
// Runtime: undefined - nhưng TypeScript không cảnh báo gì cả

Use case thực tế

Pattern này xuất hiện ở khắp nơi trong codebase thực:

  • Lọc null/undefined khỏi mảng: items.filter((x): x is NonNullable<typeof x> => x != null)
  • API response: mảng ResponseData | undefined từ fetch - cần filter và giữ type sạch
  • Phân loại user: isAdminUser(user): user is AdminUser trước khi truy cập .permissions
  • UI components đa dạng: mảng hỗn hợp nhiều widget type, lọc ra đúng loại cần render
  • URL params: ?tab=donestring | undefined - type guard narrow về 'done' | 'pending'

Lịch sử & design decision

Đây không phải bug mà là design decision có chủ đích. GitHub issue #20812 (mở từ 2017) yêu cầu TypeScript tự động infer type predicate từ logic bên trong hàm. TypeScript team từ chối: "We won't infer that e => e.text is a type predicate without an annotation."

Lý do kỹ thuật: để auto-infer, compiler phải phân tích toàn bộ logic bên trong mọi callback - cực kỳ phức tạp và dễ tạo ra kết quả sai. Issue này bị lock lại tháng 10/2025 và không có trên roadmap.

Workaround thay thế cộng đồng hay dùng: .flatMap() (ES2019) đôi khi bypass được lỗi này, nhưng nhiều người cho đó là dùng sai tool.

Khi nào dùng gì

Tình huốngNên dùng
Filter/find từ union type arrayType predicate (value is T)
Bạn kiểm soát data structureDiscriminated union + switch (auto-narrow)
Cần throw khi dữ liệu saiAssertion function (asserts value is T)
Cần parse từ unknown type-safeDowncast function

Takeaway

Rule đơn giản để nhớ: TypeScript tin vào type annotation, không tin vào runtime logic. Nếu bạn muốn compiler biết kết quả filter là gì, bạn phải nói cho nó biết bằng type predicate tường minh.

Mỗi khi viết .filter(x => ...) hay .find(x => ...) và nhận về union type rộng hơn mong đợi, đó là dấu hiệu cần thêm : x is SpecificType.

Nguồn: TypeScript Docs - Narrowing, Spencer Miskoviak, thoughtspile.github.io, TypeScript GitHub #20812.