Skip to content

Commit 39795dc

Browse files
committed
Proofread: first pass
1 parent 429f1ba commit 39795dc

File tree

1 file changed

+74
-67
lines changed

1 file changed

+74
-67
lines changed

docs/typescript.md

Lines changed: 74 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,20 @@ These guidelines specifically apply to TypeScript.
88

99
TypeScript is very good at inferring types. It is capable of providing strict type safety while ensuring that explicit type annotations are the exception rather than the rule.
1010

11-
Some fundamental type information must always be supplied by the user, such as function and class signatures, interfaces for interacting with external entities, and types that express the domain model of the codebase.
11+
Some fundamental type information must always be supplied by the user, such as function and class signatures, interfaces for interacting with external entities or data types, and types that express the domain model of the codebase.
1212

1313
However, for the remaining majority of types and values, **type inference should generally be preferred over type annotations and assertions**.
1414

1515
There are several reasons for this:
1616

17-
#### Advantages
17+
##### Type inference should be preferred over explicit annotations and assertions
1818

1919
- Explicit type annotations (`:`) and type assertions (`as`, `!`) prevent inference-based narrowing of the user-supplied types.
20-
- The compiler errs on the side of trusting user input, which prevents it from providing additional, and sometimes crucial, type information that it could otherwise infer.
21-
<!-- TODO: Expand into entry. Add example. -->
22-
- In TypeScript v4.9+ the `satisfies` operator can be used to assign type constraints that are narrowable through type inference.
20+
- The compiler errs on the side of trusting user input, which prevents it from providing additional type information that it could infer.
21+
- In TypeScript v4.9+, the `satisfies` operator can be used to assign type constraints that are narrowable through type inference.
22+
<!-- TODO: Add examples -->
2323
- Type inferences are responsive to changes in code without requiring user input, while annotations and assertions rely on hard-coding, making them brittle against code drift.
24-
- The `as const` operator can be used to further narrow an inferred abstract type into a specific literal type.
24+
- The `as const` operator can be used to narrow an inferred abstract type into a specific literal type.
2525

2626
🚫 Type declarations
2727

@@ -30,7 +30,7 @@ const name: string = 'METAMASK'; // Type 'string'
3030

3131
const BUILT_IN_NETWORKS = new Map<string, `0x${string}`>([
3232
['mainnet', '0x1'],
33-
['goerli', '0x5'],
33+
['sepolia', '0xaa36a7'],
3434
]); // Type 'Map<string, `0x${string}`>'
3535
```
3636

@@ -41,11 +41,11 @@ const name = 'METAMASK'; // Type 'METAMASK'
4141

4242
const BUILT_IN_NETWORKS = {
4343
mainnet: '0x1',
44-
goerli: '0x5',
45-
} as const; // Type { readonly mainnet: '0x1'; readonly goerli: '0x5'; }
44+
sepolia: '0xaa36a7',
45+
} as const; // Type { readonly mainnet: '0x1'; readonly sepolia: '0xaa36a7'; }
4646
```
4747

48-
##### Annotations can cause inaccurate typing and incorrectly resolved errors
48+
##### Type annotations prevent inference-based narrowing of user-supplied types
4949

5050
```typescript
5151
type TransactionMeta = TransactionBase &
@@ -85,7 +85,7 @@ const updatedTransactionMeta: TransactionMeta = {
8585

8686
```typescript
8787
// Type narrower than 'TransactionMeta': { status: TransactionStatus.rejected; ... }
88-
// (doesn't include 'error' property)
88+
// Doesn't include 'error' property. Correctly narrowed between `TransactionMeta` discriminated union
8989
const updatedTransactionMeta = {
9090
...transactionMeta,
9191
status: TransactionStatus.rejected as const,
@@ -94,13 +94,13 @@ const updatedTransactionMeta = {
9494

9595
### Type Narrowing
9696

97-
There is a clear exception to the above: if an explicit type annotation or assertion can narrow an inferred type further, thereby improving its accuracy, it should be applied.
97+
An explicit type annotation or assertion should not be avoided if they can further narrow an inferred type.
9898

99-
##### Avoid widening a type with a type annotation
99+
##### Avoid unintentionally widening a type with a type annotation
100100

101101
> **Warning**<br />
102-
> Double-check that a declared type is narrower than the inferred type.<br />
103-
> Enforcing an even wider type defeats the purpose of adding an explicit type annotation, as it _loses_ type information instead of adding it.
102+
> Enforcing an even wider type defeats the purpose of adding an explicit type annotation, as it _loses_ type information instead of adding it.<br />
103+
> Double-check that the declared type is narrower than the inferred type.
104104
105105
🚫
106106

@@ -120,7 +120,7 @@ const chainId = this.messagingSystem(
120120

121121
##### When instantiating an empty container type, provide a type annotation
122122

123-
This is one case where type inference is unable to reach a useful conclusion without user-provided information. Since the compiler cannot arbitrarily restrict the range of types that could be inserted into the container, it has to assume the widest type, which is often `any`. It's up to the user to narrow that into the intended type with an explicit type annotation.
123+
This is one case where type inference is unable to reach a useful conclusion without user-provided information. Since the compiler cannot arbitrarily restrict the range of types that could be inserted into the container, it has to assume the widest type, which is often `any`. It's up to the user to narrow that into the intended type by adding an explicit annotation.
124124

125125
🚫
126126

@@ -162,25 +162,27 @@ function f(x: SomeInterface | SomeOtherInterface) {
162162

163163
`as` assertions are unsafe. They overwrite type-checked and inferred types with user-supplied types that suppress compiler errors.
164164

165+
They should only be introduced into the code if the accurate type is unreachable through other means.
166+
165167
##### Document safe or necessary use of type assertions
166168

167-
When a type assertion is absolutely necessary due to constraints or is even safe due to runtime checks, we should document the reason for doing so.
169+
When a type assertion is absolutely necessary due to constraints or is even safe due to runtime checks, we should document the reasoning behind its usage in the form of a comment.
168170

169171
<!-- TODO: Add example -->
170172

171173
#### Avoid `as`
172174

173175
Type assertions make the code brittle against changes.
174176

175-
While TypeScript will throw type errors against some unsafe, structurally unsound, or redundant type assertions, it will generally accept the user-supplied type without type-checking.
177+
While TypeScript and ESLint will flag some unsafe, structurally unsound, or redundant type assertions, they will generally accept user-supplied types without further type-checking.
176178

177-
This can cause silent failures where errors are suppressed, even though the type's relationship to the rest of the code, or the type itself, may have been altered so that the type assertion is no longer valid.
179+
This can cause silent failures or false negatives where errors are suppressed. This is especially damaging as the codebase accumulates changes over time. Type assertions may continue to silence errors, even though the type itself, or the type's relationship to the rest of the code may have been altered so that the asserted type is no longer valid.
178180

179181
##### Redundant or unnecessary `as` assertions are not flagged for removal
180182

181-
```ts
182-
import { getKnownPropertyNames } from '@metamask/utils';
183+
Type assertions can also cause false positives, because they assertions are independent expressions, untied to the type errors they were intended to fix. Thus, even if code drift fixes or removes a particular type error, the type assertions that were put in place to fix that error will provide no indication that they are no longer necessary and now should be removed.
183184

185+
```ts
184186
enum Direction {
185187
Up = 'up',
186188
Down = 'down',
@@ -189,20 +191,9 @@ enum Direction {
189191
}
190192
const directions = Object.values(Direction);
191193
192-
// Element implicitly has an 'any' type because index expression is not of type 'number'.(7015)
193-
for (const key of Object.keys(directions)) {
194-
const direction = directions[key];
195-
}
196-
// Fix 1: use `as` assertion
197-
for (const key of Object.keys(directions)) {
198-
const direction = directions[key as keyof typeof directions];
199-
}
200-
// Fix 2: use `getKnownPropertyNames`
201-
for (const key of getKnownPropertyNames(directions)) {
202-
const direction = directions[key];
203-
}
204-
// Redundant `as` assertion does not trigger any warning
205-
for (const key of getKnownPropertyNames(directions)) {
194+
// Error: Element implicitly has an 'any' type because index expression is not of type 'number'.(7015)
195+
// Only one of the two `as` assertions necessary to fix error, but neither are flagged as redundant.
196+
for (const key of Object.keys(directions) as keyof directions[]) {
206197
const direction = directions[key as keyof typeof directions];
207198
}
208199
```
@@ -213,17 +204,31 @@ for (const key of getKnownPropertyNames(directions)) {
213204

214205
Unsafe as type assertions may be, they are still categorically preferable to using `any`.
215206

216-
- With type assertions, we still get working intellisense, autocomplete, and other IDE features.
217-
- Type assertions also provide an indication of the expected type as intended by the author.
207+
- With type assertions, we still get working intellisense, autocomplete, and other IDE and compiler features using the asserted type.
208+
- Type assertions also provide an indication of what the expected type is as intended by the author.
218209
- For type assertions to an incompatible shape, use `as unknown as` as a last resort rather than `any` or `as any`.
219210

220-
##### To define user-defined type guards
211+
##### For TypeScript syntax other than type assertion
221212

222-
`as` syntax is often required to write type guards. See https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates.
213+
- Writing type guards often reqiures using the `as` keyword.
223214

224-
##### To type data objects whose shape and contents are determined at runtime
215+
```ts
216+
function isFish(pet: Fish | Bird): pet is Fish {
217+
return (pet as Fish).swim !== undefined;
218+
}
219+
```
220+
221+
- Key remapping in mapped types uses the `as` keyword.
225222

226-
Preferably, this typing should be accompanied by schema validation performed with type guards and unit tests.
223+
```ts
224+
type MappedTypeWithNewProperties<Type> = {
225+
[Properties in keyof Type as NewKeyType]: Type[Properties];
226+
};
227+
```
228+
229+
##### To type data objects whose shape and contents are determined at runtime, externally, or through deserialization
230+
231+
Preferably, this typing should be accompanied by runtime schema validation performed with type guards and unit tests.
227232

228233
- e.g. The output of `JSON.parse()` or `await response.json()` for a known JSON input.
229234
- e.g. The type of a JSON file.
@@ -250,18 +255,39 @@ Otherwise, only mocking the properties needed in the test improves readability b
250255
- `any` suppresses all error messages about its assignee. This includes errors that are changed or newly introduced by alterations to the code. This makes `any` the cause of dangerous **silent failures**, where the code fails at runtime but the compiler does not provide any prior warning.
251256
- `any` subsumes all other types it comes into contact with. Any type that is in a union, intersection, is a property of, or has any other relationship with an `any` type or value is erased and becomes an `any` type itself.
252257

253-
<!-- TODO: Add examples -->
254-
255258
```typescript
259+
// Type of 'payload_0': 'any'
256260
const handler:
257261
| ((payload_0: ComposableControllerState, payload_1: Patch[]) => void)
258-
| ((payload_0: any, payload_1: Patch[]) => void); // Type of 'payload_0': 'any'
262+
| ((payload_0: any, payload_1: Patch[]) => void);
259263
```
260264

265+
<!-- TODO: Add more examples -->
266+
261267
- `any` pollutes all surrounding and downstream code.
262268
<!-- TODO: Add examples -->
263269

264-
##### Don't allow generic types to use `any` as a default argument
270+
##### Try `unknown` and `never` instead
271+
272+
###### `unknown`
273+
274+
- `unknown` is the universal supertype i.e. the widest possible type, equivalent to the universal set(U).
275+
- Every type is assignable to `unknown`, but `unknown` is only assignable to `unknown`.
276+
- When typing the _assignee_, `any` and `unknown` are completely interchangeable since every type is assignable to both.
277+
- `any` usage is often motivated by a need to find a placeholder type that could be anything. `unknown` is the most likely type-safe substitute for `any` in these cases.
278+
279+
###### `never`
280+
281+
- `never` is the universal subtype i.e. the narrowest possible type, equivalent to the null set(∅).
282+
- `never` is assignable to every type, but the only type that is assignable to `never` is `never`.
283+
- When typing the _assigned_:
284+
- `unknown` is unable to replace `any`, as `unknown` is only assignable to `unknown`.
285+
- The type of the _assigned_ must be a subtype of the _assignee_.
286+
- `never` is worth trying, as it is the universal subtype and assignable to all types.
287+
288+
<!-- TODO: Add examples -->
289+
290+
##### Don't allow `any` to be used as a generic default argument
265291

266292
Some generic types use `any` as a default generic argument. This can silently introduce an `any` type into the code, causing unexpected behavior and suppressing useful errors.
267293

@@ -299,26 +325,6 @@ mockGetNetworkConfigurationByNetworkClientId.mockImplementation(
299325
// Target signature provides too few arguments. Expected 2 or more, but got 1.ts(2345)
300326
```
301327

302-
##### Try `unknown` and `never` instead
303-
304-
###### `unknown`
305-
306-
- `unknown` is the universal supertype i.e. the widest possible type.
307-
- Every type is assignable to `unknown`, but `unknown` is only assignable to `unknown`.
308-
- When typing the _assignee_, `any` and `unknown` are completely interchangeable since every type is assignable to both.
309-
- `any` usage is often motivated by a need to find a placeholder type that could be anything. `unknown` is the most likely type-safe substitute for `any` in these cases.
310-
311-
###### `never`
312-
313-
- `never` is the universal subtype i.e. the narrowest possible type.
314-
- `never` is assignable to every type, but the only type that is assignable to `never` is `never`.
315-
- When typing the _assigned_:
316-
- `unknown` is unable to replace `any`, as `unknown` is only assignable to `unknown`.
317-
- The type of the _assigned_ must be a subtype of the _assignee_.
318-
- `never` is worth trying, as it is the universal subtype and assignable to all types.
319-
320-
<!-- TODO: Add examples -->
321-
322328
#### Acceptable usages of `any`
323329

324330
##### Assigning new properties to a generic type at runtime
@@ -340,8 +346,9 @@ delete addressBook[chainId as any];
340346
341347

342348
```typescript
343-
for (const key of getKnownPropertyNames(this.config)) {
344-
(this as unknown as typeof this.config)[key] = this.config[key];
349+
for (const key of getKnownPropertyNames(this.internalConfig)) {
350+
(this as unknown as typeof this.internalConfig)[key] =
351+
this.internalConfig[key];
345352
}
346353
347354
delete addressBook[chainId as unknown as `0x${string}`];

0 commit comments

Comments
 (0)