Skip to content

Commit 8c2fbe0

Browse files
committed
stash
1 parent 0eab0d6 commit 8c2fbe0

File tree

1 file changed

+234
-3
lines changed

1 file changed

+234
-3
lines changed

docs/typescript.md

Lines changed: 234 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,134 @@
22

33
These guidelines specifically apply to TypeScript.
44

5-
## Provide explicit return types for functions/methods
5+
## I. Types
6+
7+
- Type Inference
8+
- Type Annotations
9+
- `:` operator
10+
- `as const` keyword
11+
- `satisfies` operator
12+
- Generic type arguments (`<>`)
13+
- Type Assertions
14+
- `as` operator
15+
- `!` operator
16+
- Compiler Directives for Disabling Type Checking
17+
- `@ts-expect-error` directive
18+
- `@ts-ignore` directive
19+
- `any` keyword
20+
21+
### A. Type Inference vs. Type Declarations
22+
23+
In general, type inference is preferable over type declarations and , unless explicit typing results in narrower, more accurate types.
24+
25+
- Type annotations (`:`) and type assertions (`as`, `!`) prevent inference-based narrowing of the user-supplied type.
26+
- Type inferences are responsive to changes in code, while annotations and assertions rely on brittle hard-coding.
27+
28+
🚫 Type declarations
29+
30+
```ts
31+
const name: string = 'METAMASK'; // Type 'string'
32+
const BUILT_IN_NETWORKS = new Map<string, `0x${string}`>([
33+
['mainnet', '0x1'],
34+
['goerli', '0x5'],
35+
]); // Type 'Map<string, `0x${string}`>'
36+
```
37+
38+
✅ Type inferences
39+
40+
```ts
41+
const name = 'METAMASK'; // Type 'METAMASK'
42+
const BUILT_IN_NETWORKS = {
43+
mainnet: '0x1',
44+
goerli: '0x5',
45+
} as const; // Type { readonly mainnet: '0x1'; readonly goerli: '0x5'; }
46+
```
47+
48+
### B. Type Narrowing
49+
50+
That said, if there is opportunity to narrow a type with an explicit type declaration, it should be taken.
51+
52+
##### `as const`, `satisfies` can be used to further narrow inferred types
53+
54+
- Link to `as const` entry
55+
- `satisfies`: coming soon
56+
57+
##### When initializing an empty container type, provide a type annotation
58+
59+
- The compiler cannot arbitrarily restrict the range of types that can be inserted into the container, and therefore needs to assume the widest type.
60+
61+
```ts
62+
const tokens = []; // 🚫 Type 'any[]'
63+
const tokens: string[] = []; // ✅ Type 'string[]'
64+
```
65+
66+
#### Generic parameters can be used to narrow types
67+
68+
- A type with generic parameter(s) that can be omitted may sometimes be written to correctly infer the parameter's type, but it may just as often use the widest possible type that satisfies the generic constraint as a default fallback value. This fallback can sometimes be `any`.
69+
70+
🚫
71+
72+
```ts
73+
arr.reduce((acc, { name, state }) => {
74+
acc[name] = state
75+
return acc
76+
}, {} as Record<string, (typeof arr)[number]['state']>)
77+
78+
const NETWORKS = new Map({
79+
'mainnet': '0x1',
80+
'goerli': '0x5',
81+
}); // Type 'Map<any, any>'
82+
83+
const mockGetNetworkConfigurationByNetworkClientId = jest.fn(); // Type 'jest.Mock<any, any>'
84+
```
85+
86+
87+
88+
```ts
89+
arr.reduce<Record<arr[number]['name'], arr[number]['state']>>((acc, { name, state }) => {
90+
acc[name] = state
91+
return acc
92+
}, {});
93+
94+
const NETWORKS = new Map<string, `0x${string}`>({
95+
'mainnet': '0x1',
96+
'goerli': '0x5',
97+
}); // Type 'Map<string, `0x${string}`>'
98+
99+
const mockGetNetworkConfigurationByNetworkClientId = jest.fn<
100+
ReturnType<NetworkController['getNetworkConfigurationByNetworkClientId']>,
101+
Parameters<NetworkController['getNetworkConfigurationByNetworkClientId']>
102+
>(); // Type 'jest.Mock<NetworkConfiguration | undefined, [networkClientId: string]>'
103+
```
104+
105+
##### Type guards and null checks can be used to improve type inference
106+
107+
TypeScript types are erased (https://en.wikipedia.org/wiki/Type_erasure) during compilation. This means there is no built-in mechanism for performing runtime type checks.
108+
109+
A popular method for runtime checks is to verify that properties exist on an object. You can use user-defined type guards to accomplish this:
110+
111+
```ts
112+
function isSomeInterface(x: unknown): x is SomeInterface {
113+
return 'name' in x
114+
&& typeof x.name === 'string'
115+
&& 'length' in x
116+
&& typeof x.length === 'number';
117+
}
118+
119+
function f(x: SomeInterface | SomeOtherInterface) {
120+
if (isSomeInterface(x)) {
121+
console.log(x.name); // Cool!
122+
}
123+
}
124+
```
125+
126+
##### For functions and methods, provide explicit return types
6127

7128
Although TypeScript is capable of inferring return types, adding them explicitly makes it much easier for the reader to see the API from the code alone and prevents unexpected changes to the API from emerging.
8129

9130
🚫
10131

11-
```javascript
132+
```ts
12133
async function removeAccount(address: Hex) {
13134
const keyring = await this.getKeyringForAccount(address);
14135

@@ -25,7 +146,7 @@ async function removeAccount(address: Hex) {
25146

26147
27148

28-
```javascript
149+
```ts
29150
async function removeAccount(address: Hex): Promise<KeyringControllerState> {
30151
const keyring = await this.getKeyringForAccount(address);
31152

@@ -39,3 +160,113 @@ async function removeAccount(address: Hex): Promise<KeyringControllerState> {
39160
return this.fullUpdate();
40161
}
41162
```
163+
164+
- Provide an explanation via comment when performing explicit type assertions or overriding related eslint rules (e.g. forbid `any`, forbid `non-null assertions`)
165+
166+
### C. Type Assertions (`as`, `!`)
167+
168+
`as` assertions and non-nullability assertions (`!`) are unsafe. They override type-checked and inferred types with user-supplied types that suppress compiler errors.
169+
170+
While TypeScript will throw type errors against some unsafe or structurally unsound type assertion scenarios, it will generally accept the user-supplied type without type-checking. This can cause silent failures where errors are suppressed.
171+
172+
- TypeScript and eslint will raise warnings against some unsafe or structurally unsound type assertions, but this capability is limited.
173+
- Type assertions make the code brittle against changes,
174+
175+
- `as` assertions intended to exclude an empty/nullable type (e.g. `| undefined`) can be replaced with a nullish coalescing operator providing an acceptable fallback empty value that doesn't pollute the variable's type signature.
176+
177+
```ts
178+
arr: string[] | undefined
179+
(arr as string[]) // 🚫
180+
(arr ?? []) //
181+
182+
s: string | undefined
183+
(s as string) // 🚫
184+
(s ?? '') //
185+
```
186+
187+
#### Acceptable use cases of `as`
188+
189+
##### To prevent or fix `any` usage
190+
191+
- At least we get working intellisense, autocomplete.
192+
- We also get an indication of the expected type as intended by the author.
193+
- For type assertions to an incompatible shape, use `as unknown as` as a last resort.
194+
195+
##### To define user-defined type guards
196+
197+
- https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
198+
199+
##### To type data objects whose shape and contents are determined at runtime
200+
201+
- provided that schema validation is performed with type guards and unit tests
202+
- e.g. The output of `JSON.parse()` or `await response.json()` for a known JSON input.
203+
- e.g. The type of a JSON file.
204+
205+
##### In tests, for mocking or to exclude irrelevant but required properties from an input object
206+
207+
- Recommended: Provide accurate typing wherever possible.
208+
209+
#### Acceptable use cases of `!`
210+
211+
##### If type narrowing is not inferring the correct type
212+
213+
##### If the variable is otherwise guaranteed to be non-nullable
214+
215+
- Preferrably, leave a comment explaining why.
216+
217+
### D. Compiler Directives for Disabling Type Checking (`any`, `@ts-ignore`, `@ts-expect-error`)
218+
219+
- `any` doesn't actually represent the widest type. It's a compiler setting to *disable* type checking for the value or type to which it's assigned.
220+
221+
- If `any` is used, and errors are introduced (or altered) by future changes to the code,
222+
- The new or changed warnings will be suppressed, and the code will **fail silently**.
223+
224+
- `any` infects all surrounding and downstream code.
225+
- For instance, if you have a function that is declared to return any which actually returns an object, all properties of that object will be any.
226+
227+
- `unknown` is the universal supertype i.e. the widest possible type.
228+
- When typing the assignee, `any` and `unknown` are interchangeable (every type is assignable to both).
229+
- When typing the assigned, `unknown` can't replace `any`, as `unknown` is only assignable to `unknown`.
230+
- If replacing `any` with `unknown` doesn't work, the typing likely has underlying issues that *shouldn't be silenced*.
231+
232+
#### Acceptable use cases of `any`
233+
234+
##### Assigning new properties to a generic type at runtime
235+
236+
- In most cases, `any` usage can be avoided by substituting with `as unknown as`.
237+
238+
```ts
239+
for (const key of Object.keys(this.internalConfig) as (keyof C)[]) {
240+
(this as unknown as C)[key] = this.internalConfig[key];
241+
}
242+
```
243+
244+
- However, when assigning to a generic type, using `as any` is the only solution, unless the assignment itself can be avoided.
245+
246+
```ts
247+
(state as RateLimitState<RateLimitedApis>).requests[api][origin] =
248+
previous + 1;
249+
// is generic and can only be indexed for reading.ts(2862)
250+
251+
(state as any).requests[api][origin] =
252+
previous + 1;
253+
```
254+
255+
##### Within generic constraints
256+
257+
```ts
258+
messenger extends RestrictedControllerMessenger<N, any, any, string, string>
259+
```
260+
261+
- In general, using `any` in this context is not harmful in the same way that it is in other contexts, as the `any` types only are not directly assigned to any specific variable, and only function as constraints.
262+
- That said, narrower constraints provide better type safety and intellisense.
263+
264+
##### Catching errors
265+
266+
- `catch` only accepts `any` and `unknown` as the error type.
267+
- Recommended: Use `unknown` with type guards like `isJsonRpcError`.
268+
- Avoid typing an error object with `any` if it is used elsewhere instead of just being thrown.
269+
270+
##### In tests, for mocking or to intentionally break features
271+
272+
- Recommended: Provide accurate typing wherever possible.

0 commit comments

Comments
 (0)