Cách để khai báo 1 prop không được chấp nhận hoặc cảnh báo không tồn tại Full (ok)
https://medium.com/innovation-and-technology/deciphering-typescripts-react-errors-8704cc9ef402
Last updated
https://medium.com/innovation-and-technology/deciphering-typescripts-react-errors-8704cc9ef402
Last updated
components\Test.tsx
components\Test.tsx
·
Theo
Đã xuất bản trongĐổi mới và Công nghệ·16 phút đọc·Ngày 28 tháng 6 năm 2018
2,2 nghìn
11
Kiểm tra kiểu tĩnh là một trong những biện pháp bảo vệ kỹ thuật yêu thích của tôi, đó là lý do tại sao tôi ưu tiên áp dụng trình kiểm tra kiểu khi chọn bộ công cụ phát triển ứng dụng web trong nhóm Kỹ thuật số của Thành phố Boston .
Kiểm tra kiểu tĩnh giảm rủi ro bảo trì phần mềm, vì nó chỉ ra những nơi bạn cần cập nhật mã để phù hợp với những thay đổi bạn đang thực hiện. Nó cũng tốt cho phát triển mới vì, với sự hỗ trợ của trình soạn thảo tốt, nó đảm bảo bạn đang gọi các hàm hiện có và cung cấp cho chúng các đối số mà chúng mong đợi.
Nhưng thứ sáu tuần trước, tôi nhận được tin nhắn trực tiếp từ đồng nghiệp John, người đã viết một số thành phần React:
Tôi ghét những lỗi đó, tôi định nói là không thích nhưng naaaaahhh tôi ghét chúng lol
John là một sinh viên mới tốt nghiệp trại huấn luyện, làm việc với tôi vào mùa hè với tư cách là một thành viên. Anh ấy cũng rất thoải mái, vì vậy việc nhìn thấy phản ứng mạnh mẽ này thực sự đánh thức tôi.
Tôi đã ném John vào thế giới kiểm tra kiểu mà không có sự chuẩn bị. Anh ấy đã trở thành nạn nhân của một nghịch lý về rào chắn : việc xử lý các thông báo lỗi "hữu ích" của trình kiểm tra có thể tốn nhiều công sức hơn là gỡ lỗi các vấn đề mà trình kiểm tra cảnh báo bạn.
Sự kết hợp của React, JSX và DOM tạo ra một số loại TypeScript phức tạp. Khi TypeScript phàn nàn, thông báo lỗi của nó rất dài dòng, với rất nhiều tên mà bạn không nhận ra từ mã của riêng bạn. Vì vậy, đối với John và tất cả những người mới làm quen với môi trường này, tôi xin giới thiệu hướng dẫn của mình để giải mã thông báo lỗi React / TypeScript của bạn.
(Bạn có thể nhận thấy rằng ứng dụng web Registry được viết bằng Flow . Đối với phần phát triển mới, chúng tôi đang làm việc bằng TypeScript .)
Hãy cùng xem chúng ta hy vọng đạt được điều gì khi sử dụng TypeScript với React ngay từ đầu. Khi mọi thứ diễn ra suôn sẻ, chúng ta sẽ bắt được những lỗi này:
Cố gắng truyền một prop mà một thành phần không muốn
Quên truyền một prop mà một thành phần yêu cầu
Nhận sai loại của một prop, chẳng hạn như truyền một chuỗi khi nó mong đợi một số
Nếu chúng ta viết những lỗi này, TypeScript sẽ hiển thị lỗi ngay trong trình soạn thảo của chúng ta. Nếu không có TypeScript, chúng ta sẽ phải phát hiện những lỗi này sau trong quá trình thử nghiệm và có thể sẽ rất tẻ nhạt khi phải gỡ lỗi để tìm ra lỗi xuất phát từ đâu.
(Có đáng để sử dụng công cụ để phát hiện những lỗi này hay không là câu hỏi dành cho nhóm của bạn. Tôi thấy nó rất hữu ích, đó là lý do chúng tôi ở đây.)
Để bắt đầu, hãy xem xét việc sử dụng các phần tử DOM chuẩn trong JSX. TypeScript sẽ kiểm tra để đảm bảo rằng mọi thuộc tính bạn đặt vào thẻ HTML đều tồn tại và có đúng loại. Ví dụ:
Kiểu mã React/JSX ở trên kiểm tra tốt, vì <input>
s có cả thuộc tính type
và name
! Bạn sẽ không thấy bất kỳ lỗi nào. TypeScript có thể làm điều này vì có một thư viện React ( @types/react
) định nghĩa tất cả các phần tử HTML và các thuộc tính mà mỗi phần tử có.
Lưu ý: Trong hướng dẫn này, tôi sử dụng "thuộc tính" và "thuộc tính" thay thế cho nhau. "Thuộc tính" là tên gọi của chúng trong HTML, nhưng trên các thành phần React, chúng là "props". Và khi chúng nằm trên Đối tượng JavaScript, TypeScript gọi chúng là "thuộc tính".
Bây giờ chúng ta hãy viết sai chữ “ name
” và xem điều gì xảy ra:
Nếu trình soạn thảo của bạn hỗ trợ TypeScript, bạn sẽ thấy dòng đó có gạch chân màu đỏ và thông báo lỗi sau:
Có vẻ hơi khó khăn, nhưng chúng ta có thể phân tích từng phần một:
Property 'nmae'
— “Thuộc tính” là các thuộc tính trên <input>
thẻ. Vì vậy, lỗi này liên quan đến “ nmae=”…"
”.
does not exist on type
— Có một “kiểu”, một định nghĩa ở đâu đó về những gì được phép có trên <input>
, và “ nmae
” không có trong danh sách.
'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'
— OK, đây là một điều khó khăn. Điều này mô tả cách TypeScript nghĩ về <input>
các thuộc tính của ’. Tất cả các tên này ( DetailedHTMLProps
, InputHTMLAttributes
, và HTMLInputElement
) được định nghĩa trong thư viện React đó. DetailedHTMLProps
có nghĩa là các thuộc tính mà bất kỳ phần tử HTML nào cũng có thể có (như id
, tabIndex
, và style
) và InputHTMLAttributes
dành cho các thuộc tính cụ thể cho <input>
các phần tử ( name
, value
, &c. ). Đây là bản mẫu mà bạn thường có thể bỏ qua, nhưng HTML***Element
phần này có thể là một manh mối hữu ích để giải mã điều này.
Bản sửa lỗi: Khi bạn thấy “ Property XXX does not exist on type
,” nghĩa là bạn đang thêm an XXX="…"
vào một phần tử không muốn có an XXX
trên đó. Mặc dù bản thân điều đó không phải là vấn đề—phần tử sẽ chỉ bỏ qua các thuộc tính mà nó không nhận ra—nhưng thường thì đó là dấu hiệu của lỗi. Trong trường hợp này, việc thêm a không phải là vấn đề nmae
, nhưng đó là vấn đề mà chúng ta nghĩ rằng mình đã thêm a name
nhưng thực tế lại không phải vậy.
Khi bạn thấy điều này, hãy kiểm tra lỗi đánh máy và kiểm tra tài liệu HTML để đảm bảo bạn có thuộc tính hoặc phần tử phù hợp với mục đích bạn đang cố gắng thực hiện.
Có một lỗi khác có thể xảy ra với HTML: sử dụng sai kiểu cho một thuộc tính. Hãy thêm a size
vào <input>
và xem điều gì xảy ra:
Đây chính xác là cách bạn viết trong trang HTML, nhưng TypeScript lại hiển thị lỗi:
Types of property 'size' are incompatible.
— Chúng ta đang nói về size
hiện tại. Lưu ý cách nó nói "types" ở dạng số nhiều. Đó là vì tất cả các lỗi kiểu đều là sự không nhất quán . Lỗi này nằm giữa hai kiểu: một kiểu từ mã của chúng ta và một kiểu từ <input>
định nghĩa của React (hãy nhớ lại DetailedHTMLProps<…>
phần chúng ta đã thấy trong lỗi trước).
Type 'string'
— Đây là kiểu đầu tiên, kiểu cho đối số mà chúng ta đã viết, "6"
là một chuỗi, vì tất cả các thuộc tính JSX được trích dẫn đều là chuỗi.
is not assignable to
— Đây là cách diễn đạt tinh tế của từ “không phải”. Nó làm rõ điều “không tương thích” giữa hai loại.
type 'number | undefined'
— Đây là kiểu thứ hai trong hai kiểu, kiểu mà thư viện React cho là phù hợp với thuộc tính <input>
's size
. The |
được gọi là toán tử “union”, nhưng bạn có thể đọc là “or”: “ size
phải là một số hoặc không xác định”.
Sửa lỗi: Chúng ta cần thay đổi một trong hai kiểu để làm cho chúng nhất quán. Vì chúng ta không thể thay đổi thư viện React để chấp nhận một chuỗi, chúng ta phải thay đổi mã của mình để truyền một số:
Hãy nhớ rằng đó {}
là cách tạo biểu thức JavaScript trong JSX. Bây giờ thay vì chuỗi, chúng ta cung cấp số và lỗi TypeScript sẽ biến mất.
Thông báo lỗi cho biết kiểu từ thư viện React là 'number | undefined'
. “Undefined” giống như việc bỏ nó đi, đó là lý do tại sao các ví dụ trước của chúng tôi đã kiểm tra kiểu mà không có size
. Nhưng điều này giải thích tại sao thông báo lỗi cho biết “is not assigned to” thay vì “isn't.” 6
là một số, không phải là “number or undefined.” Nhưng, một số có thể gán cho “number or undefined,” vì “or.”
Thư viện kiểu React không yêu cầu bất kỳ thuộc tính nào trên các thành phần HTML, do đó không có lỗi "quên truyền prop mà một thành phần yêu cầu" trong phần này. Lỗi này sẽ xuất hiện khi chúng ta xử lý các thành phần tùy chỉnh.
Tôi đã đề cập đến điều này ở trên trong phần phân tích size
lỗi, nhưng đây là một khái niệm quan trọng đến mức tôi muốn nhắc lại:
Tất cả các lỗi kiểu đều là sự không nhất quán .
Ý tôi là những dòng chữ TypeScript màu đỏ đó ở đó vì một phần trong mã của bạn không phù hợp với phần khác. Phần nào đúng? Công việc của bạn là tìm ra điều đó bằng cách suy nghĩ về những gì bạn đang cố gắng làm cho mã của mình thực hiện.
Một trong những lý do khiến lỗi kiểu có thể gây nhầm lẫn khi diễn giải là TypeScript sẽ chỉ gạch chân một trong những nơi xảy ra sự không nhất quán. Ví dụ đầu tiên bên dưới là lỗi khi TypeScript gọi ra một cái gì đó trong JSX, nhưng phần đó thực sự đúng và chúng ta quyết định định nghĩa kiểu là sai.
Nguyên tắc kiểm tra kiểu của các thành phần tùy chỉnh về cơ bản giống với các phần tử DOM, nhưng tên của các kiểu lại khó hiểu hơn.
Đối với phần này, chúng tôi sẽ sử dụng thành phần mẫu sau:
Giả sử chúng ta sử dụng this để xây dựng một form, vì vậy chúng ta cần truyền vào một name
phương thức có thể đi vào đó <input>
. Trong React, điều đó có nghĩa là thêm một prop. Đây là render
phương thức mới:
Có thể được sử dụng bởi một thành phần khác như thế này:
Có vẻ đơn giản, và trong JavaScript thông thường trường hợp này hoàn toàn có thể hoạt động, không có lỗi. Nhưng TypeScript đã tìm thấy điều gì đó để phàn nàn:
Property 'fieldName'
— Chúng ta đã thấy ngôn ngữ này trước đây với thuộc tính <input>
's nmae
, nhưng bây giờ nó đề cập đến một “thuộc tính” trên this.props
đối tượng, thay vì một thuộc tính trên thẻ HTML.
does not exist on type
— Đây cũng là cùng một ngôn ngữ. TypeScript có ý kiến về những thuộc tính nào là và không phải là trên this.props
, và fieldName
nằm trong đống “không”.
'Readonly<{ children?: ReactNode; }> & Readonly<{}>'
— Được rồi, điều này khó đọc. Đây là kiểu mà TypeScript nghĩ đến khi chúng ta nói this.props
.
Phần cuối cùng cần được giải thích chi tiết hơn:
s Readonly
có nghĩa là bạn không thể thay đổi this.props
. Bạn không nên làm điều đó với React, vì vậy TypeScript thực thi điều đó.
{ children?: ReactNode; }
có ở đó không vì tất cả các thành phần React đều có thể có this.props.children
giá trị. ReactNode
là bất kỳ thứ gì mà React biết cách hiển thị: chuỗi, thành phần, v.v. Điều đó ?
có nghĩa là thuộc tính này là tùy chọn—bạn không cần phải truyền các thành phần con cho CustomInput
.
&
trong TypeScript tạo ra một "giao điểm" giữa hai kiểu, giống như một đối tượng có tất cả các thuộc tính từ mỗi kiểu được hợp nhất với nhau. Vì vậy, this.props
có một kiểu được tạo thành từ tất cả các thuộc tính của { children?: ReactNode; }
kết hợp với tất cả các thuộc tính của {}
. Mà không có bất kỳ thuộc tính nào.
Điều này có nghĩa là this.props
có một children
thuộc tính tùy chọn nhưng không có fieldName
thuộc tính nào.
Cách khắc phục: Để khắc phục điều này, chúng ta thực sự phải đưa ra lựa chọn. Hãy nói cùng tôi ngay bây giờ:
Tất cả các lỗi kiểu đều là sự không nhất quán .
Có hai điều không đồng ý. Trong render
phương pháp của bạn, bạn đang cố gắng sử dụng this.props.fieldName
. Nhưng khi bạn khai báo thành phần của mình, bạn đã không cung cấp cho nó một fieldName
prop. Cái nào đúng? render
Phương pháp hay khai báo thành phần?
Với các phần tử DOM, chúng ta biết rằng các thuộc tính mong đợi là chính xác vì chúng đến từ một thư viện mà chúng ta tin tưởng. Nhưng đây là mã của riêng chúng ta, vì vậy chúng ta phải chọn những gì cần thay đổi để lỗi biến mất. Điều đó có nghĩa là ngừng cố gắng sử dụng this.props.fieldName
, hoặc thêm fieldName
vào khai báo thành phần của chúng ta.
Theo mặc định, tất cả React.Component
s đều có this.props
kiểu {}
, mà chúng ta thấy trong &
câu lệnh trong lỗi. Vì chúng ta thực sự muốn có một fieldName
prop, chúng ta phải thêm nó:
Chuyện gì đang xảy ra ở đây? Chúng ta bắt đầu với interface
, đó là cách chúng ta tạo một "kiểu đối tượng" mới trong TypeScript. Chúng ta đang nói rằng bất kỳ đối tượng nào có kiểu CustomInputProps
phải là "một đối tượng có thuộc fieldName
tính, phải là một chuỗi".
CustomInputProps
là một kiểu đối tượng, có nghĩa là nó mô tả “hình dạng” của một đối tượng JavaScript. Ví dụ:
Đó là một const
lệnh được gọi obj
và : CustomInputProps
cho TypeScript biết rằng giá trị của nó cần phải khớp với CustomInputProps
hình dạng. Chúng tôi khởi tạo nó bằng một đối tượng theo nghĩa đen có fieldName
thuộc tính cần thiết, do đó nó kiểm tra kiểu!
Chúng ta hãy bỏ qua fieldName
phần bất động sản và xem điều gì xảy ra:
{}
không khớp CustomInputProps
, vì vậy đây là cách TypeScript phàn nàn:
Type '{}'
— Đây là TypeScript mô tả kiểu giá trị mà chúng ta đang cố gắng gán cho obj
: một đối tượng không có trường nào.
is not assignable to
— Người bạn cũ của chúng ta “không thể gán cho”. Chúng ta đang cố gắng thực hiện một phép gán: gán cho {}
, obj
nhưng TypeScript lại nói rằng chúng ta không thể làm điều đó.
type 'CustomInputProps'.
— Đây là vì nó là thứ chúng ta đã tuyên bố obj
. Do đó, nó là kiểu được gán cho .
Property 'fieldName' is missing in type '{}'.
— Bây giờ TypeScript đang nêu rõ lý do tại sao một đối tượng có kiểu {}
không thể gán cho thứ gì đó có kiểu CustomInputProps
: Không có fieldName
, và, theo interface
khai báo ở trên, tất cả CustomInputProps
các đối tượng phải có fieldName
.
Bây giờ chúng ta hãy xem điều gì xảy ra khi chúng ta đưa trở lại fieldName
, nhưng cũng thêm một trường không phải là một phần của CustomInputProps
kiểu:
Điều đó không được phép:
Type '{ fieldName: string; placeholder: string; }'
— Đây là kiểu mới của đối tượng theo nghĩa đen của chúng ta. Hãy xem cách nó có placeholder
thuộc tính này.
is not assignable to type 'CustomInputProps'.
— Giống như trước. TypeScript cho rằng lệnh gán này là bất hợp pháp.
Object literal may only specify known properties, and 'placeholder' does not exist in type 'CustomInputProps'.
— Đây là lời giải thích cho lý do tại sao trường hợp này “không thể gán được.” CustomInputProps
không có placeholder
thuộc tính được khai báo trong interface
khối của nó.
Đây là trường hợp tương tự như khi chúng ta thêm nmae
. <input>
Việc thêm thuộc tính bổ sung placeholder
không phải là lỗi, vì mã đang chạy CustomInputProps
sẽ bỏ qua nó, nhưng đó là dấu hiệu cho thấy có thể có lỗi.
Quay lại lý do tại sao chúng ta lại nói về các kiểu đối tượng ngay từ đầu. Chúng liên quan gì đến React props? Đối tượng có {}
dấu ngoặc nhọn ngoằn ngoèo, không phải <>
dấu ngoặc nhọn của JSX. Để giải thích điều này, hãy quay lại ví dụ về phần tử DOM của chúng ta và nói một chút về JSX.
Trình duyệt không thể tự hiểu JSX này, chúng chỉ hiểu JavaScript thuần túy. Chúng tôi thường sử dụng trình biên dịch TypeScript hoặc một công cụ như Babel để lấy JSX và tạo JavaScript từ đó. Sau đây là đoạn mã trông như thế nào dưới dạng JavaScript:
Này, những thứ đó name="color"
và size={6}
các thuộc tính bây giờ là một đối tượng theo nghĩa đen! Đối số đầu tiên createElement
là những gì chúng ta đang tạo ra—một lớp cho các thành phần tùy chỉnh, một tên thẻ cho các phần tử HTML—và đối số thứ hai là các props để truyền vào nó.
Đây là lý do tại sao TypeScript luôn nói về props như các kiểu đối tượng. Khi kiểm tra kiểu mã React, nó đảm bảo rằng đối số thứ hai createElement
có kiểu khớp với (“có thể gán cho”) kiểu đã khai báo của props của thành phần.
(Vì nó chỉ chuyển đổi mọi thứ thành JavaScript thuần túy, nên JSX đôi khi được gọi là "cú pháp ngọt ngào". Thực ra, nó không cần thiết—chúng ta có thể createElement
tự viết những câu lệnh đó—nhưng nó làm cho mã trở nên ngọt ngào hơn.)
Hãy cùng nhớ lại chúng ta đã ở đâu:
Bây giờ chúng ta đã khai báo the interface
we want—“hình dạng” của props của thành phần—chúng ta thực sự cần phải bảo TypeScript sử dụng nó cho CustomInput
. Đó là nơi the React.Component<CustomInputProps>
xuất hiện. Các <…>
bit cho phép chúng ta truyền tham số kiểu . Hãy nghĩ về nó như một cách để điều chỉnh React.Component
, giống như cách bạn sử dụng (…)
s để truyền đối số cho các hàm. ReactComponent
sử dụng tham số kiểu đầu tiên của nó làm kiểu for this.props
, thay thế mặc định {}
.
Chúng ta có thể quay lại render
phương pháp của mình và xem this.props
hiện tại là gì. Trong Visual Studio Code, bạn có thể di chuột qua nó:
React.Component<CustomInputProps, {}, any>
— Đây là Visual Studio cho chúng ta biết điều gì this
. Nó gần giống với extends React.Component<CustomInputProps>
khai báo lớp của chúng ta, chỉ với các giá trị mặc định cho CustomInputProps
2 tham số kiểu bổ sung được viết rõ ràng.
.props:
— Bởi vì đó chính là thứ chúng ta đang quan tâm.
Readonly<{ children?: React.ReactNode; }> & Readonly<CustomInputProps>
— Bạn còn nhớ Readonly
thứ này từ trước không? Bây giờ thì nó CustomInputProps
thay thế cho {}
.
Tuyệt! Với thay đổi này, this.props
vẫn giữ nguyên thuộc tính trước đó children
nhưng giờ cũng có mọi thứ từ CustomInputProps
, đặc biệt là fieldName
thuộc tính.
Sử dụng this.state
gần giống với this.props
, vì vậy tôi sẽ không đi sâu vào nó. Các lỗi bạn nhận được từ nó về cơ bản sẽ giống nhau. Để cung cấp cho trạng thái của thành phần một loại, hãy sử dụng tham số loại thứ hai cho React.Component
, như thế này:
Ví dụ trên cũng cho thấy việc khởi tạo state
với giá trị mặc định. Bạn cũng có thể thực hiện việc này this.state = {…}
trong constructor()
phương thức. Tôi cũng đã rút ngắn tên giao diện thành “ Props
” và “ State
” để chúng không bị bao bọc. Chúng thực sự có thể là bất kỳ thứ gì, miễn là bạn nhất quán giữa interface
và React.Component<…>
.
Hãy quay lại sử dụng CustomInput
thành phần của chúng ta. Điều này sẽ tương tự như khi chúng ta tạo <input>
các phần tử.
Hãy nhớ lại chỗ chúng ta dừng lại CustomInput
:
Chúng ta hãy cùng làm một cái nhé!
Và lỗi là…
Mọi chuyện ngày càng trở nên phức tạp hơn, phải không?
Type '{}'
— Để giải thích điều này, hãy nhớ cách JSX hoạt động. Dòng mã trên của chúng ta chuyển thành React.createElement(CustomInput, {})
. {}
vì chúng ta không viết bất kỳ props nào trong JSX. Type '{}'
TypeScript đang mô tả đối số props thứ hai đó.
is not assignable to
— Vẫn là cách cũ. Có sự không nhất quán trong các loại!
'IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }>...'
— Đây là một mớ hỗn độn đến mức TypeScript thực sự từ bỏ và dừng lại trước khi đến cuối. Các bit "nội tại" bao gồm các đạo cụ React phổ biến như key
và ref
. Bạn có thể thấy Readonly<{ children?: ReactNode; }>
ở cuối, đó là phần bắt đầu của this.props
kiểu mà chúng ta đã thấy trước đó. Kiểu hoàn chỉnh ở đây là: IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }> & Readonly<CustomInputProps>
, nhưng thành thật mà nói, bạn có thể bỏ qua dòng này.
Type '{}' is not assignable to type 'Readonly<CustomInputProps>'
— Ở đây TypeScript gọi ra phần nào của IntrinsicAttributes & … & …
nó đang gặp sự cố. {}
có thể gán hoàn hảo cho IntrinsicAttributes
, IntrinsicClassAttributes
, và Readonly<{ children?: ReactNode; }>
vì tất cả các trường của chúng đều là tùy chọn. Phần không thể gán được là CustomInputProps
.
Property 'fieldName' is missing in type '{}'.
— Đây là phần hữu ích nhất của thông báo lỗi. Nó type '{}'
gọi lại createElement
đối số thứ hai, loại props từ JSX.
Sửa lỗi: Một lần nữa, lỗi kiểu là đánh dấu sự không nhất quán trong mã. Như đã viết, CustomInput
muốn có một fieldName
prop, nhưng bạn không cung cấp cho nó. Vì vậy, hãy cung cấp cho nó một prop, hoặc làm cho nó không muốn có prop ngay từ đầu. Hãy xem cách chúng ta sẽ thực hiện từng cái.
Để vượt qua fieldName
, chúng ta làm như sau:
Sau khi dịch JSX, nó trở thành:
Kiểu của đối số props thứ hai là { fieldName: string }
có thể gán cho CustomInputProps
.
Đây là trường hợp khác, làm cho fieldName
tùy chọn. Đối với trường hợp này, chúng ta cần thay đổi where interface
đã CustomInputProps
được khai báo.
Bạn thấy đấy, có một ?
there ở đây không? Điều đó cho TypeScript biết rằng thuộc tính này là tùy chọn. Bây giờ cả hai { fieldName: string }
và {}
đều có thể gán cho CustomInputProps
.
Điều này có nghĩa là this.props.fieldName
bây giờ có kiểu 'string | undefined'
, vì vậy TypeScript sẽ khiến bạn tính đến điều đó trong phần còn lại của mã. Không gọi this.props.fieldName.length
mà không kiểm tra xem nó đã được định nghĩa trước chưa!
Cuối cùng, hãy xem các thông báo lỗi bạn nhận được khi bạn cung cấp một prop cho một thành phần tùy chỉnh không nằm trong loại props của nó. TypeScript có hai thông báo lỗi khác nhau, tùy thuộc vào các props khác mà bạn truyền.
Thông báo lỗi này giống với thông báo chúng ta nhận được nmae
trong ví dụ đầu, chỉ có CustomInput
kiểu props dài hơn ' ở cuối. Thật không may là thông báo này bị cắt trước khi nó nói CustomInputProps
, vì đó là một phần thông tin thực sự giúp theo dõi những gì đang diễn ra. Chỉ cần nhớ rằng khi bạn thấy thông báo này, rất có thể prop bạn đang truyền không được định nghĩa trong giao diện props của chính thành phần.
Thật kỳ lạ, nếu chúng ta bỏ đi fieldName
thì thông báo lỗi sẽ khác:
Chúng ta có thể thấy { placeholder: string; }
kiểu từ createElement
câu lệnh ẩn sau hậu trường và IntrinsicAttributes & …
kiểu cho CustomInput
props của 's. Chỉ là vì một lý do nào đó mà tôi không biết TypeScript nói "không có thuộc tính chung" thay vì "không tồn tại".
Cách khắc phục: Giải quyết sự không nhất quán! Thêm placeholder
vào CustomInputProps
hoặc xóa nó khỏi <CustomInput … />
.
Đây rồi. Giải thích về các lỗi TypeScript phổ biến nhất mà bạn gặp phải trong React JSX và cách bạn có thể giải quyết chúng. Hy vọng điều đó sẽ làm giảm bớt sự tức giận trong tương lai khi lập trình.
Tôi thừa nhận rằng việc tìm ra những cách mới để kiểm tra kiểu để bắt lỗi là một trong những bài tập lập trình yêu thích của tôi. Trong cơ sở mã Boards and Commissions mới , chúng tôi đang sử dụng ts2gql
để biến giao diện TypeScript thành lược đồ GraphQL, nghĩa là chúng tôi có thể sử dụng các giao diện TypeScript đó để kiểm tra kiểu rất chính xác cho trình phân giải của mình . Cũng giống như với ứng dụng web Registry, chúng tôi đang sử dụng apollo-codegen
để tạo đối số và kiểu đầu ra cho các truy vấn GraphQL và chúng tôi cũng đã thêm vào sql-ts
để tạo kiểu TypeScript từ các bảng SQL Server.
Đây là lời nhắc nhở hữu ích đối với tôi và bất kỳ ai đang lắp ráp dụng cụ, rằng lan can bảo vệ chỉ hiệu quả khi bạn biết nó đang bảo vệ cái gì.
Guillaume Marceau , khi còn là sinh viên sau đại học, đã giúp tôi hiểu cách lỗi kiểu phát sinh từ hai phần xung đột của một chương trình. James Duffy đã hiệu đính bài viết này.
Và tất nhiên là cảm ơn John vì đã vượt qua tất cả những điều này một cách mạnh mẽ.
·
Follow
Published inInnovation and Technology·16 min read·Jun 28, 2018
2.2K
11
Static type checking is one of my favorite engineering guard rails, which is why I made adopting a typechecker a priority when choosing the toolset for webapp development on the City of Boston’s Digital team.
Static type checking de-risks software maintenance, since it reveals places where you need to update code to accommodate changes you’re making. It’s equally good for new development since, with good editor support, it ensures you’re calling functions that exist and are giving them the arguments they expect.
But, last Friday, I got a DM from my co-worker John, who has been writing some React components:
I hate those errors, I was going to say dislike but naaaaahhh I hate them lol
John’s a recent bootcamp grad, working with me for the summer as a fellow. He’s also super-chill, so seeing this strong reaction really woke me up.
I had thrown John into a type-checking world without preparation. He fell victim to a paradox of guard rails: it can take more effort to deal with a checker’s “helpful” error messages than it would ever be to debug the problems it’s warning you against.
The combination of React, JSX, and the DOM make for some hairy TypeScript types. When TypeScript complains, its error messages are verbose, with a lot of names you don’t recognize from your own code. So, for John and everyone else new to this environment, I present my guide to decoding your React / TypeScript error messages.
(You might notice that the Registry webapp was written with Flow. For new development we’re working in TypeScript.)
Let’s see what we hope to get out of using TypeScript with React in the first place. When things go smoothly, we’ll be catching these bugs:
Trying to pass a prop that a component doesn’t want
Forgetting to pass a prop that a component requires
Getting a prop’s type wrong, such as passing a string when it expects a number
If we write these bugs, TypeScript will show an error in our editor right away. Without TypeScript, we’d have to catch these bugs later during testing, and it might be tedious debugging to figure out where they come from.
(Whether or not it’s worth it to use a tool to catch these bugs is a question for your team. I find it valuable, which is why we’re here.)
For starters, let’s look at using standard DOM elements in JSX. TypeScript will check to make sure that every attribute you put on an HTML tag exists and is of the right type. For example:
The above React/JSX code type checks fine, because <input>
s have both type
and name
attributes! You won’t see any errors. TypeScript can do this because there’s a React library (@types/react
) that defines all HTML elements and what attributes they each take.
Note: In this guide I use “attribute” and “property” pretty much interchangeably. “Attribute” is what they’re called in HTML, but on React components they’re “props.” And, when they’re on a JavaScript Object, TypeScript calls them “properties.”
Now let’s mis-spell “name
” and see what happens:
If you have TypeScript support in your editor, you should get a red underline on that line, and the following error message:
That looks a little daunting, but we can break it down piece-by-piece:
Property 'nmae'
— The “properties” are the attributes on the <input>
tag. So this error is about “nmae=”…"
”.
does not exist on type
— There’s a “type,” a definition somewhere of what is allowed to be on <input>
, and “nmae
” isn’t on the list.
'DetailedHTMLProps<InputHTMLAttributes<HTMLInputElement>, HTMLInputElement>'
— OK, this is a doozy. This describes how TypeScript thinks of<input>
’s attributes. All of these names (DetailedHTMLProps
, InputHTMLAttributes
, and HTMLInputElement
) are defined in that React library. DetailedHTMLProps
means attributes that any HTML element can have (like id
, tabIndex
, and style
) and InputHTMLAttributes
are for ones specific to <input>
elements ( name
, value
, &c.). This is boilerplate you can typically gloss over, but the HTML***Element
part can be a useful clue in deciphering this.
The fix: When you see “Property XXX does not exist on type
,” it means you’re adding an XXX="…"
to an element that doesn’t want an XXX
on it. While that is not itself a problem—the element would just ignore attributes it doesn’t recognize—it’s usually a sign of a bug. In this case, it’s not a problem to add a nmae
, but it is a problem that we thought we were adding a name
but didn’t actually.
When you see this, check for typos, and check the HTML docs to make sure you’ve got the attribute or element right for what you’re trying to do.
There’s another error that can come up with HTML: using the wrong type for an attribute. Let’s add a size
to the <input>
and see what happens:
This is exactly how you’d write it in an HTML page, but TypeScript shows an error:
Types of property 'size' are incompatible.
— We’re talking about size
now. Note how it says “types” with a plural. That’s because all type errors are inconsistencies. This error is between two types: the one from our code, and the one from React’s <input>
definition (remember theDetailedHTMLProps<…>
bit we saw in the last error).
Type 'string'
— This is the first of the types, the one for the argument that we wrote. "6"
is a string, because all quoted JSX attributes are strings.
is not assignable to
— This is a nuanced way of saying “isn’t.” It’s clarifying what is “incompatible” about the two types.
type 'number | undefined'
— This is the second of the two types, the one that the React library said is right for <input>
’s size
attribute. The |
is known as a “union” operator, but you can read it as “or”: “size
must be a number or undefined.”
The fix: We need to change one or the other of the types to make them consistent. Since we can’t change the React library to accept a string, we have to change our code to pass a number:
Remember that {}
is the way to make a JavaScript expression in JSX. Now instead of a string, we’re providing a number, and the TypeScript error goes away.
The error message said that the type from the React library was 'number | undefined'
. “Undefined” is the same as leaving it off, which is why our previous examples typechecked without a size
. But this explains why the error message said “is not assignable to” instead of “isn’t.” 6
is a number, not a “number or undefined.” But, a number is assignable to a “number or undefined,” because of the “or.”
The React type library doesn’t require any attributes on HTML elements, so there’s no “forgetting to pass a prop that a component requires” error in this section. That will come up when we deal with custom components.
I mentioned this above in the breakdown of the size
bug, but it’s an important enough concept that I want to call it out:
All type errors are inconsistencies.
What I mean by this is those red TypeScript squiggles are there because one part of your code doesn’t agree with another part. Which part is right? Your job is to figure that out by thinking about what you’re trying to make your code do.
One of the reasons that type errors can be confusing to interpret is that TypeScript will only underline one of the places where the inconsistency is happening. The first example below is an error where TypeScript calls out something in the JSX, but that part is actually right and it’s the type definition that we decide is wrong.
The principles of typechecking custom components are basically the same as DOM elements, but the names of the types get more confusing.
For this section, we’ll use the following sample component:
Say we’re using this to build a form, so we need to pass in a name
that can go on that <input>
. In React, that means adding a prop. Here’s the new render
method:
Which might get used by another component like this:
Seems straightforward, and in plain JavaScript this case would totally work, no bugs. But TypeScript has found something to complain about:
Property 'fieldName'
— We saw this language before with the <input>
’s nmae
attribute, but now it’s referring to a “property” on the this.props
object, rather than one on an HTML tag.
does not exist on type
— This is also the same language. TypeScript has an opinion about what properties are and are not on this.props
, and fieldName
is in the “not” pile.
'Readonly<{ children?: ReactNode; }> & Readonly<{}>'
— OK, this is hard to read. This is the type that TypeScript thinks of when we say this.props
.
That last bit needs a more detailed explanation:
The Readonly
s mean that you can’t change this.props
. You shouldn’t be doing that with React, so TypeScript enforces it.
{ children?: ReactNode; }
is there because all React components can have a this.props.children
value. ReactNode
is anything that React knows how to render: strings, components, and so on. The ?
means that this property is optional—you don’t need to pass children to CustomInput
.
&
in TypeScript makes an “intersection” between two types, which is kind of like an object that has all the properties from each of them merged together. So, this.props
has a type that’s made up of all the properties of { children?: ReactNode; }
combined with all the properties of {}
. Which doesn’t have any.
That comes together to mean that this.props
has an optional children
property, but no fieldName
property.
The fix: To fix this, we actually have to make a choice. Say it with me now:
All type errors are inconsistencies.
Two things don’t agree. In your render
method you’re trying to use this.props.fieldName
. But when you declared your component, you didn’t give it a fieldName
prop. Which is right? The render
method or the component declaration?
With DOM elements, we knew that the expected attributes were correct because they came from a library we trust. But this is our own code, so it’s up to us to pick what to change to make the error go away. That means either stop trying to use this.props.fieldName
, or add fieldName
to our component’s declaration.
By default, all React.Component
s have a this.props
type of {}
, which we saw in the &
statement in the error. Since we really do want to have a fieldName
prop, we have to add it:
What’s going on here? We start with interface
, which is how we make a new “object type” in TypeScript. We’re saying that any object of type CustomInputProps
must be “an object with a fieldName
property, which must be a string.”
CustomInputProps
is an object type, which means that it describes the “shape” of a JavaScript object. For example:
That’s a const
called obj
, and : CustomInputProps
tells TypeScript that its value needs to match the CustomInputProps
shape. We initialize it with an object literal that has the necessary fieldName
property, so it typechecks!
Let’s leave off the fieldName
property and see what happens:
{}
doesn’t match CustomInputProps
, so here’s how TypeScript complains:
Type '{}'
— This is TypeScript describing the type of the value that we’re trying to assign to obj
: an object with no fields.
is not assignable to
— Our old friend “is not assignable to.” We’re trying to make an assignment: assigning {}
to obj
, but TypeScript is saying we can’t do it.
type 'CustomInputProps'.
— This is here because it’s what we declared obj
to be. Therefore it’s the type that’s being assigned to.
Property 'fieldName' is missing in type '{}'.
— Now TypeScript is being specific about why an object of type {}
is not assignable to something of type CustomInputProps
: There’s no fieldName
, and, because of the interface
declaration from above, all CustomInputProps
objects must have a fieldName
.
Now let’s see what happens when we put back fieldName
, but also add a field that’s not part of the CustomInputProps
type:
That’s not allowed:
Type '{ fieldName: string; placeholder: string; }'
— This is the new type of our object literal. See how it now has the placeholder
property.
is not assignable to type 'CustomInputProps'.
— Same as before. TypeScript thinks this assignment is illegal.
Object literal may only specify known properties, and 'placeholder' does not exist in type 'CustomInputProps'.
— This is the explanation for why this case is “not assignable.” CustomInputProps
doesn’t have a placeholder
property declared in its interface
block.
This is a similar case to when we put nmae
on an <input>
. Adding the extra placeholder
property is not in itself a bug, since code working on CustomInputProps
would ignore it, but it’s a sign that there’s probably a bug.
Back to why we’re even talking about object types in the first place. What do they have to do with React props? Objects have {}
squiggly braces, not the <>
angle brackets of JSX. To explain this, let’s go back to our DOM element example and talk a bit about JSX.
Browsers can’t understand this JSX on their own, they only understand plain JavaScript. We typically use the TypeScript compiler or a tool like Babel to take the JSX and make JavaScript out of it. Here’s what that code looks like as JavaScript:
Hey, those name="color"
and size={6}
attributes are an object literal now! The first argument to createElement
is what we’re creating—a class for custom components, a tag name for HTML elements—and the second argument are the props to pass to it.
This is why TypeScript is always talking about props as object types. When it type checks the React code, it makes sure that that second argument to createElement
has a type that matches (“is assignable to”) the declared type of the component’s props.
(Because it just transforms things into plain JavaScript, JSX is sometimes referred to as “syntactic sugar.” It’s not actually necessary—we could write those createElement
statements ourselves—but it makes the code sweeter.)
Let’s remember where we were:
Now that we’ve declared the interface
we want—the “shape” of our component’s props—we need to actually tell TypeScript to use it for CustomInput
. That’s where the theReact.Component<CustomInputProps>
comes in. The <…>
bits let us pass a type parameter. Think of it as a way of tweaking React.Component
, kind of like how you use (…)
s to pass arguments to functions. ReactComponent
uses its first type parameter as the type for this.props
, replacing the default {}
.
We can go back to our render
method and see what this.props
is now. In Visual Studio Code, you can hover over it:
React.Component<CustomInputProps, {}, any>
— This is Visual Studio telling us what this
is. It closely matches the extends React.Component<CustomInputProps>
from our class declaration, just with the default values for CustomInputProps
’s 2 additional type parameters written out explicitly.
.props:
— Because that’s what we’re hovering over.
Readonly<{ children?: React.ReactNode; }> & Readonly<CustomInputProps>
— Remember this Readonly
stuff from before? Now it has CustomInputProps
instead of {}
.
Cool! With this change, this.props
keeps the previous children
property, but now also has everything from CustomInputProps
, in particular the fieldName
property.
Using this.state
is nearly identical to this.props
, so I won’t go into it in depth. The errors you get from it should be basically the same. To give a component’s state a type, use the second type parameter to React.Component
, like so:
The above example also shows initializing state
with a default value. You could also do this with this.state = {…}
in the constructor()
method. I’ve also shortened the interface names to “Props
” and “State
” so they don’t wrap. They can actually be anything, as long as you’re consistent between the interface
and the React.Component<…>
.
Let’s go back to using our CustomInput
component. This will be similar to when we were making <input>
elements.
Remember where we left off with CustomInput
:
Let’s make one!
And the error is…
It just keeps getting more complicated, doesn’t it?
Type '{}'
— To explain this, remember how JSX works. Our above line of code turns into React.createElement(CustomInput, {})
. {}
because we didn’t write any props in the JSX. Type '{}'
is TypeScript describing that second, props argument.
is not assignable to
— The old standby. There’s an inconsistency in the types!
'IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }>...'
— This is such a mess that TypeScript literally gives up and stops before it gets to the end. The “intrinsic” bits cover common React props like key
and ref
. You can see the Readonly<{ children?: ReactNode; }>
at the end, which is the start of the this.props
type we saw before. The complete type here is: IntrinsicAttributes & IntrinsicClassAttributes<CustomInput> & Readonly<{ children?: ReactNode; }> & Readonly<CustomInputProps>
, but honestly you can glaze over this line.
Type '{}' is not assignable to type 'Readonly<CustomInputProps>'
— Here TypeScript calls out what part of the IntrinsicAttributes & … & …
it’s having trouble with. {}
is perfectly assignable to IntrinsicAttributes
, IntrinsicClassAttributes
, and Readonly<{ children?: ReactNode; }>
because all of their fields are optional. What it’s not assignable to is our CustomInputProps
.
Property 'fieldName' is missing in type '{}'.
— This is the most useful part of the error message. The type '{}'
is calling back to that second createElement
argument, the type of the props from the JSX.
The fix: Once again, type errors are flagging inconsistencies in the code. As written, CustomInput
wants a fieldName
prop, but you’re not giving it one. So, either give it one, or make it not want one in the first place. Let’s look at how we would do each.
To pass fieldName
, we do this:
After the JSX translation, that turns into:
The type of that second, props argument is { fieldName: string }
which is assignable to CustomInputProps
.
Here’s the other case, making fieldName
optional. For this, we need to change the interface
where CustomInputProps
was declared.
See how there’s a ?
there now? That tells TypeScript that this property is optional. Now both { fieldName: string }
and {}
are assignable to CustomInputProps
.
This does mean that this.props.fieldName
now has the type 'string | undefined'
, so TypeScript will make you take that into account in the rest of your code. No calling this.props.fieldName.length
without checking that it’s defined first!
Finally, let’s look at the error messages you get when you give a prop to a custom component that isn’t in its props type. TypeScript has two different error messages, depending on what other props you pass.
This error message is the same one we got with nmae
in the early example, just with CustomInput
’s longer props type at the end. It is very unfortunate that this is cut off before it says CustomInputProps
, because that’s a piece of information that would really help track down what’s going on. Just remember that when you see this, it’s most likely that the prop you’re passing it isn’t defined in the component’s own props interface.
Curiously, if we leave off fieldName
, the error message is different:
We can see the { placeholder: string; }
type from the createElement
statement that’s behind the scenes, and the IntrinsicAttributes & …
type for CustomInput
’s props. It’s just for a reason I don’t know TypeScript says “has no properties in common” rather than “does not exist.”
The fix: Resolve the inconsistency! Either add placeholder
to CustomInputProps
or remove it from <CustomInput … />
.
There we go. Explanations for the most common TypeScript errors you run into in your React JSX, and how you might solve them. Hopefully that will reduce any future rage when programming.
I will admit that finding new ways for type checking to catch errors is one of my favorite programming exercises. In the new Boards and Commissions codebase, we’re using ts2gql
to turn TypeScript interfaces into a GraphQL schema, which means we can use those TypeScript interfaces to very precisely typecheck our resolvers. Just as with the Registry webapp, we’re using apollo-codegen
to generate argument and output types for GraphQL queries, and we’ve also added sql-ts
to make TypeScript types from SQL Server tables.
It’s a good reminder to me, and anyone else who’s putting tooling together, that a guard rail is only as good as whether or not you can tell what it’s guarding against.
Guillaume Marceau, way back in his grad school days, helped me understand how type errors arise from two conflicting parts of a program. James Duffy proofread this article.
And of course to John, for powering through all of this like a champ.