Skip to content

Commit b99ee25

Browse files
authored
ref(utils): Use type predicates in is utility functions (#4124)
We have a number of `is` utility functions (`isString`, `isError`, and so on), and while they tell the human reading the code something about the value in question, they don't tell TypeScript anything, because it doesn't see them as type checks. As a result, we end up with a lot of code like this: ``` if( isString(msg) ) { const words = (msg as string).split(' '); } ``` where we have to typecast our variables, despite their type already having been checked immediately beforehand. In order to fix this, TS lets you return a type predicate[1] from a type-checking function rather than a simple boolean, and that lets it know what the human reader knows - if `isString(msg)` is true, then `msg` must be a string. This switches all of our `is` functions to use type predicates, and removes all of the typecasts which are no longer necessary as a result of this change. [1] https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
1 parent 76b8231 commit b99ee25

File tree

13 files changed

+40
-36
lines changed

13 files changed

+40
-36
lines changed

packages/browser/src/eventbuilder.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,15 +65,14 @@ export function eventFromUnknownInput(
6565
): Event {
6666
let event: Event;
6767

68-
if (isErrorEvent(exception as ErrorEvent) && (exception as ErrorEvent).error) {
68+
if (isErrorEvent(exception)) {
6969
// If it is an ErrorEvent with `error` property, extract it to get actual Error
70-
const errorEvent = exception as ErrorEvent;
7170
// eslint-disable-next-line no-param-reassign
72-
exception = errorEvent.error;
73-
event = eventFromStacktrace(computeStackTrace(exception as Error));
71+
exception = exception.error;
72+
event = eventFromStacktrace(computeStackTrace(exception));
7473
return event;
7574
}
76-
if (isDOMError(exception as DOMError) || isDOMException(exception as DOMException)) {
75+
if (isDOMError(exception) || isDOMException(exception)) {
7776
// If it is a DOMError or DOMException (which are legacy APIs, but still supported in some browsers)
7877
// then we just extract the name, code, and message, as they don't provide anything else
7978
// https://developer.mozilla.org/en-US/docs/Web/API/DOMError
@@ -90,9 +89,9 @@ export function eventFromUnknownInput(
9089

9190
return event;
9291
}
93-
if (isError(exception as Error)) {
92+
if (isError(exception)) {
9493
// we have a real Error object, do nothing
95-
event = eventFromStacktrace(computeStackTrace(exception as Error));
94+
event = eventFromStacktrace(computeStackTrace(exception));
9695
return event;
9796
}
9897
if (isPlainObject(exception) || isEvent(exception)) {

packages/integrations/src/ember.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ export class Ember implements Integration {
5858
getCurrentHub().withScope(scope => {
5959
if (isInstanceOf(reason, Error)) {
6060
scope.setExtra('context', 'Unhandled Promise error detected');
61-
getCurrentHub().captureException(reason, { originalException: reason as Error });
61+
getCurrentHub().captureException(reason, { originalException: reason });
6262
} else {
6363
scope.setExtra('reason', reason);
6464
getCurrentHub().captureMessage('Unhandled Promise error detected');

packages/integrations/src/extraerrordata.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ export class ExtraErrorData implements Integration {
9797
continue;
9898
}
9999
const value = error[key];
100-
extraErrorInfo[key] = isError(value) ? (value as Error).toString() : value;
100+
extraErrorInfo[key] = isError(value) ? value.toString() : value;
101101
}
102102

103103
// Check if someone attached `toJSON` method to grab even more properties (eg. axios is doing that)
@@ -106,7 +106,7 @@ export class ExtraErrorData implements Integration {
106106

107107
for (const key of Object.keys(serializedError)) {
108108
const value = serializedError[key];
109-
extraErrorInfo[key] = isError(value) ? (value as Error).toString() : value;
109+
extraErrorInfo[key] = isError(value) ? value.toString() : value;
110110
}
111111
}
112112

packages/nextjs/src/utils/instrumentServer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ function makeWrappedReqHandler(origReqHandler: ReqHandler): WrappedReqHandler {
227227
// If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
228228
let traceparentData;
229229
if (req.headers && isString(req.headers['sentry-trace'])) {
230-
traceparentData = extractTraceparentData(req.headers['sentry-trace'] as string);
230+
traceparentData = extractTraceparentData(req.headers['sentry-trace']);
231231
logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
232232
}
233233

packages/nextjs/src/utils/withSentry.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export const withSentry = (origHandler: NextApiHandler): WrappedNextApiHandler =
3939
// If there is a trace header set, extract the data from it (parentSpanId, traceId, and sampling decision)
4040
let traceparentData;
4141
if (req.headers && isString(req.headers['sentry-trace'])) {
42-
traceparentData = extractTraceparentData(req.headers['sentry-trace'] as string);
42+
traceparentData = extractTraceparentData(req.headers['sentry-trace']);
4343
logger.log(`[Tracing] Continuing trace ${traceparentData?.traceId}.`);
4444
}
4545

packages/node/src/backend.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ export class NodeBackend extends BaseBackend<NodeOptions> {
4141
const message = `Non-Error exception captured with keys: ${extractExceptionKeysForMessage(exception)}`;
4242

4343
getCurrentHub().configureScope(scope => {
44-
scope.setExtra('__serialized__', normalizeToSize(exception as Record<string, unknown>));
44+
scope.setExtra('__serialized__', normalizeToSize(exception));
4545
});
4646

4747
ex = (hint && hint.syntheticException) || new Error(message);

packages/node/src/handlers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ export function tracingHandler(): (
5656
// If there is a trace header set, we extract the data from it (parentSpanId, traceId, and sampling decision)
5757
let traceparentData;
5858
if (req.headers && isString(req.headers['sentry-trace'])) {
59-
traceparentData = extractTraceparentData(req.headers['sentry-trace'] as string);
59+
traceparentData = extractTraceparentData(req.headers['sentry-trace']);
6060
}
6161

6262
const transaction = startTransaction(

packages/serverless/src/awslambda.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ export function wrapHandler<TEvent, TResult>(
256256
let traceparentData;
257257
const eventWithHeaders = event as { headers?: { [key: string]: string } };
258258
if (eventWithHeaders.headers && isString(eventWithHeaders.headers['sentry-trace'])) {
259-
traceparentData = extractTraceparentData(eventWithHeaders.headers['sentry-trace'] as string);
259+
traceparentData = extractTraceparentData(eventWithHeaders.headers['sentry-trace']);
260260
}
261261
const transaction = startTransaction({
262262
name: context.functionName,

packages/serverless/src/gcpfunction/http.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function _wrapHttpFunction(fn: HttpFunction, wrapOptions: Partial<HttpFunctionWr
5353
let traceparentData;
5454
const reqWithHeaders = req as { headers?: { [key: string]: string } };
5555
if (reqWithHeaders.headers && isString(reqWithHeaders.headers['sentry-trace'])) {
56-
traceparentData = extractTraceparentData(reqWithHeaders.headers['sentry-trace'] as string);
56+
traceparentData = extractTraceparentData(reqWithHeaders.headers['sentry-trace']);
5757
}
5858
const transaction = startTransaction({
5959
name: `${reqMethod} ${reqUrl}`,

packages/tracing/src/browser/request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ export function fetchCallback(
185185
const options = (handlerData.args[1] = (handlerData.args[1] as { [key: string]: any }) || {});
186186
let headers = options.headers;
187187
if (isInstanceOf(request, Request)) {
188-
headers = (request as Request).headers;
188+
headers = request.headers;
189189
}
190190
if (headers) {
191191
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access

packages/tracing/src/transaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class Transaction extends SpanClass implements TransactionInterface {
3737
super(transactionContext);
3838

3939
if (isInstanceOf(hub, Hub)) {
40-
this._hub = hub as Hub;
40+
this._hub = hub;
4141
}
4242

4343
this.name = transactionContext.name || '';

packages/utils/src/is.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { Primitive } from '@sentry/types';
99
* @param wat A value to be checked.
1010
* @returns A boolean representing the result.
1111
*/
12-
export function isError(wat: any): boolean {
12+
export function isError(wat: any): wat is Error {
1313
switch (Object.prototype.toString.call(wat)) {
1414
case '[object Error]':
1515
return true;
@@ -29,7 +29,7 @@ export function isError(wat: any): boolean {
2929
* @param wat A value to be checked.
3030
* @returns A boolean representing the result.
3131
*/
32-
export function isErrorEvent(wat: any): boolean {
32+
export function isErrorEvent(wat: any): wat is ErrorEvent {
3333
return Object.prototype.toString.call(wat) === '[object ErrorEvent]';
3434
}
3535

@@ -40,7 +40,7 @@ export function isErrorEvent(wat: any): boolean {
4040
* @param wat A value to be checked.
4141
* @returns A boolean representing the result.
4242
*/
43-
export function isDOMError(wat: any): boolean {
43+
export function isDOMError(wat: any): wat is DOMError {
4444
return Object.prototype.toString.call(wat) === '[object DOMError]';
4545
}
4646

@@ -51,18 +51,18 @@ export function isDOMError(wat: any): boolean {
5151
* @param wat A value to be checked.
5252
* @returns A boolean representing the result.
5353
*/
54-
export function isDOMException(wat: any): boolean {
54+
export function isDOMException(wat: any): wat is DOMException {
5555
return Object.prototype.toString.call(wat) === '[object DOMException]';
5656
}
5757

5858
/**
59-
* Checks whether given value's type is a string
59+
* Checks whether the given value's type is string
6060
* {@link isString}.
6161
*
6262
* @param wat A value to be checked.
6363
* @returns A boolean representing the result.
6464
*/
65-
export function isString(wat: any): boolean {
65+
export function isString(wat: any): wat is string {
6666
return Object.prototype.toString.call(wat) === '[object String]';
6767
}
6868

@@ -84,7 +84,7 @@ export function isPrimitive(wat: any): wat is Primitive {
8484
* @param wat A value to be checked.
8585
* @returns A boolean representing the result.
8686
*/
87-
export function isPlainObject(wat: any): boolean {
87+
export function isPlainObject(wat: any): wat is { [key: string]: unknown } {
8888
return Object.prototype.toString.call(wat) === '[object Object]';
8989
}
9090

@@ -95,7 +95,7 @@ export function isPlainObject(wat: any): boolean {
9595
* @param wat A value to be checked.
9696
* @returns A boolean representing the result.
9797
*/
98-
export function isEvent(wat: any): boolean {
98+
export function isEvent(wat: any): wat is Event {
9999
return typeof Event !== 'undefined' && isInstanceOf(wat, Event);
100100
}
101101

@@ -106,7 +106,7 @@ export function isEvent(wat: any): boolean {
106106
* @param wat A value to be checked.
107107
* @returns A boolean representing the result.
108108
*/
109-
export function isElement(wat: any): boolean {
109+
export function isElement(wat: any): wat is Element {
110110
return typeof Element !== 'undefined' && isInstanceOf(wat, Element);
111111
}
112112

@@ -117,7 +117,7 @@ export function isElement(wat: any): boolean {
117117
* @param wat A value to be checked.
118118
* @returns A boolean representing the result.
119119
*/
120-
export function isRegExp(wat: any): boolean {
120+
export function isRegExp(wat: any): wat is RegExp {
121121
return Object.prototype.toString.call(wat) === '[object RegExp]';
122122
}
123123

@@ -140,6 +140,12 @@ export function isThenable(wat: any): boolean {
140140
export function isSyntheticEvent(wat: any): boolean {
141141
return isPlainObject(wat) && 'nativeEvent' in wat && 'preventDefault' in wat && 'stopPropagation' in wat;
142142
}
143+
144+
// Having the two different types is necessary because some types packages type classes as functions (in addition to
145+
// being objects with a `new` method), and some only type them as `new`-able objects
146+
type Constructor<T> = (...args: any[]) => T;
147+
type ClassWithConstructor<T> = { new (...args: any[]): T };
148+
143149
/**
144150
* Checks whether given value's type is an instance of provided constructor.
145151
* {@link isInstanceOf}.
@@ -148,7 +154,7 @@ export function isSyntheticEvent(wat: any): boolean {
148154
* @param base A constructor to be used in a check.
149155
* @returns A boolean representing the result.
150156
*/
151-
export function isInstanceOf(wat: any, base: any): boolean {
157+
export function isInstanceOf<T>(wat: any, base: Constructor<T> | ClassWithConstructor<T>): wat is T {
152158
try {
153159
return wat instanceof base;
154160
} catch (_e) {

packages/utils/src/object.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ function getWalkSource(
103103
currentTarget?: unknown;
104104
}
105105

106-
const event = value as SimpleEvent;
106+
const event = (value as unknown) as SimpleEvent;
107107

108108
const source: {
109109
[key: string]: any;
@@ -381,9 +381,8 @@ export function extractExceptionKeysForMessage(exception: any, maxLength: number
381381
* Given any object, return the new object with removed keys that value was `undefined`.
382382
* Works recursively on objects and arrays.
383383
*/
384-
export function dropUndefinedKeys<T>(val: T): T {
385-
if (isPlainObject(val)) {
386-
const obj = val as { [key: string]: any };
384+
export function dropUndefinedKeys<T>(obj: T): T {
385+
if (isPlainObject(obj)) {
387386
const rv: { [key: string]: any } = {};
388387
for (const key of Object.keys(obj)) {
389388
if (typeof obj[key] !== 'undefined') {
@@ -393,11 +392,11 @@ export function dropUndefinedKeys<T>(val: T): T {
393392
return rv as T;
394393
}
395394

396-
if (Array.isArray(val)) {
397-
return (val as any[]).map(dropUndefinedKeys) as any;
395+
if (Array.isArray(obj)) {
396+
return (obj as any[]).map(dropUndefinedKeys) as any;
398397
}
399398

400-
return val;
399+
return obj;
401400
}
402401

403402
/**

0 commit comments

Comments
 (0)