Ngăn loại giá trị trả về của callback bao gồm các thuộc tính bổ sung trong TypeScript
Giả sử tôi có một giao diện Foo và một hàm bar nhận vào một callback trả về một Foo.
interface Foo {
foo: string;
}
function bar(callback: () => Foo): Foo {
return callback();
}
Khi tôi gọi điều này, nếu tôi thêm các thuộc tính vào giá trị trả về của mình ngoài foo, trình biên dịch sẽ không đưa ra lỗi.
// throws an error as expected
const x: Foo = { foo: 'abc', baz: 'def' };
// does not throw an error
const y = bar(() => ({ foo: 'abc', baz: 'def' }));
Làm thế nào tôi có thể đảm bảo rằng đối tượng không có thuộc tính nào khác ngoài những gì được chỉ định trong Foo? Tôi đã thử sử dụng type thay vì interface với kết quả tương tự. Đây là TypeScript playground.
Câu trả lời #1
Việc kiểm tra thuộc tính thừa chỉ xảy ra trong những trường hợp cụ thể, và bạn đã tìm thấy một trường hợp mà nó không xảy ra. Các kiểu (bao gồm các interface như Foo) trong TypeScript là mở và có thể mở rộng; bạn có thể định nghĩa một interface như
interface Baz extends Foo {
baz: string;
}
và trả về một Baz bên trong callback bar(), và nó là một Foo hợp lệ:
const baz: Baz = { foo: "abc", baz: "def" };
const foo: Foo = baz; // no error
const z = bar(() => baz); // no error...
TypeScript không có một kiểu cụ thể tương ứng với chính xác Foo mà không có thuộc tính bổ sung nào. Tuy nhiên, có thể sử dụng một ràng buộc tổng quát đại diện cho "Foo mà không có thuộc tính bổ sung nào được biết". Lưu ý cảnh báo "được biết", nó sẽ xuất hiện sau:
function bar<
F extends Foo & { [K in keyof F]: K extends keyof Foo ? Foo[K] : never }
>(callback: () => F): Foo {
return callback();
}
Ở đây tôi đã biến bar() thành một hàm tổng quát (generic function) nhận một callback có kiểu () => F, trong đó F bị ràng buộc bởi Foo, cũng như một kiểu điều kiện ánh xạ (mapped conditional type). Kiểu ánh xạ này lấy bất kỳ khóa bổ sung nào đã biết và thay đổi kiểu thuộc tính thành never, điều này là không thể. Ví dụ, nếu F là Baz, thì ràng buộc sẽ là Foo & { foo: string, baz: never }. Vì Baz không mở rộng { foo: string, baz: never }, bạn sẽ nhận được lỗi nếu cố gắng gọi bar() trên một hàm được biết là trả về Baz:
const y = bar(() => ({ foo: "abc", baz: "def" })); // error, generic constraint failed
// -----------------------------> ~~~
// "string" is not assignable to "never"
const z = bar(() => baz); // error! "string" is not assignable to "never"
// ---------------> ~~~
Điều đó ít nhất sẽ ngăn mọi người trả về các literal đối tượng với các thuộc tính bổ sung.
Tuy nhiên, đây không phải là một giải pháp hoàn hảo. Biến foo trước đó được chú thích là kiểu Foo, nhưng đã được khởi tạo với một giá trị kiểu Baz. Trình biên dịch chỉ biết hoặc quan tâm rằng foo là kiểu Foo. Nó hoàn toàn quên rằng foo cũng là kiểu Baz. Do đó nó không biết rằng foo có thuộc tính "baz", và sẽ không ngăn chặn điều này:
const oops = bar(() => foo); // still no error...
// foo has a "baz" property but the compiler forgot
// no way around this in TypeScript, sorry
Tôi e là không có gì có thể làm được ở đó ngoài việc kiểm tra thời gian chạy. Nếu bạn thực sự muốn xem các kiểu chính xác trong TypeScript, bạn có thể muốn truy cập vào vấn đề tương ứng trên GitHub và nhấn 👍. Nhưng có lẽ giải pháp trên đã đủ cho trường hợp sử dụng của bạn.
À, hi vọng điều đó có ích. Chúc may mắn!
Liên kết đến mã
