Remove unsafe type assertions and manual typeof checks across frontend and server

Frontend:
- useExportProcessRecordsForCSV: add isFieldCurrencyValue guard for
  currency record values instead of unvalidated property access
- PhonesDisplay: replace typeof === 'object' with Array.isArray in
  parseAdditionalPhones, use unknown instead of any

Server:
- Remove redundant `as string[]` casts in calendar and messaging
  person listeners (6 instances) - type already narrowed by ?? []
- compute-changed-additional-emails: remove as string[] casts,
  let Array.isArray narrow the type
- transform-emails-value: remove as string[] after JSON.parse,
  rely on isNonEmptyArray for runtime validation
- find-person-by-primary-or-additional-email: use isNonEmptyArray
  from twenty-shared instead of Array.isArray
- create-company-and-contact.service: same isNonEmptyArray fix

https://claude.ai/code/session_01K2CBC59gYw93d8YVQm3mXX
This commit is contained in:
Claude
2026-04-09 20:50:19 +00:00
parent 26a8fc15e2
commit 9e7ff0a6c1
8 changed files with 37 additions and 31 deletions
@@ -1,5 +1,6 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { type FieldCurrencyValue } from '@/object-record/record-field/ui/types/FieldMetadata';
import { isFieldCurrencyValue } from '@/object-record/record-field/ui/types/guards/isFieldCurrencyValue';
import { type ObjectRecord } from '@/object-record/types/ObjectRecord';
import { isDefined } from 'twenty-shared/utils';
import { FieldMetadataType } from '~/generated-metadata/graphql';
@@ -19,16 +20,24 @@ export const useExportProcessRecordsForCSV = (objectNameSingular: string) => {
}
switch (field.type) {
case FieldMetadataType.CURRENCY:
case FieldMetadataType.CURRENCY: {
const currencyValue = record[field.name];
if (!isFieldCurrencyValue(currencyValue)) {
return processedRecord;
}
return {
...processedRecord,
[field.name]: {
amountMicros: convertCurrencyMicrosToCurrencyAmount(
record[field.name].amountMicros,
),
currencyCode: record[field.name].currencyCode,
amountMicros:
currencyValue.amountMicros !== null
? convertCurrencyMicrosToCurrencyAmount(
currencyValue.amountMicros,
)
: null,
currencyCode: currencyValue.currencyCode,
} satisfies FieldCurrencyValue,
};
}
case FieldMetadataType.MULTI_SELECT:
case FieldMetadataType.ARRAY:
case FieldMetadataType.RAW_JSON:
@@ -116,18 +116,20 @@ export const PhonesDisplay = ({
);
};
const parseAdditionalPhones = (additionalPhones?: any) => {
const parseAdditionalPhones = (additionalPhones?: unknown) => {
if (!additionalPhones) {
return [];
}
if (typeof additionalPhones === 'object') {
if (Array.isArray(additionalPhones)) {
return additionalPhones;
}
if (typeof additionalPhones === 'string') {
try {
return JSON.parse(additionalPhones);
const parsed = JSON.parse(additionalPhones);
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
logError(t`Error parsing additional phones: ${error}`);
}
@@ -17,14 +17,12 @@ export const transformEmailsValue = (
if (additionalEmails) {
try {
const emailArray = (
isNonEmptyString(additionalEmails)
? JSON.parse(additionalEmails)
: additionalEmails
) as string[];
const emailArray = isNonEmptyString(additionalEmails)
? JSON.parse(additionalEmails)
: additionalEmails;
additionalEmails = isNonEmptyArray(emailArray)
? JSON.stringify(emailArray.map((email) => email.toLowerCase()))
? JSON.stringify(emailArray.map((email: string) => email.toLowerCase()))
: null;
} catch {
/* empty */
@@ -45,8 +45,7 @@ export class CalendarEventParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -82,8 +81,7 @@ export class CalendarEventParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -115,8 +113,7 @@ export class CalendarEventParticipantPersonListener {
const personEmails = peopleHavingEmails
.flatMap((eventPayload) => [
eventPayload.properties.before.emails.primaryEmail,
...((eventPayload.properties.before.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.before.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -8,7 +8,7 @@ import {
ConnectedAccountProvider,
type FieldActorSource,
} from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { type DeepPartial, type Repository } from 'typeorm';
import { v4 } from 'uuid';
@@ -251,7 +251,7 @@ export class CreateCompanyAndPersonService {
const existingPersonOnAdditionalEmails = alreadyCreatedPeople.find(
(person) => {
return (
Array.isArray(person.emails?.additionalEmails) &&
isNonEmptyArray(person.emails?.additionalEmails) &&
person.emails.additionalEmails.some(
(email) => email.toLowerCase() === contact.handle.toLowerCase(),
)
@@ -5,8 +5,8 @@ import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/
export const computeChangedAdditionalEmails = (
diff: Partial<ObjectRecordDiff<PersonWorkspaceEntity>>,
) => {
const before = diff.emails?.before?.additionalEmails as string[];
const after = diff.emails?.after?.additionalEmails as string[];
const before = diff.emails?.before?.additionalEmails;
const after = diff.emails?.after?.additionalEmails;
if (!Array.isArray(before) || !Array.isArray(after)) {
return {
@@ -1,3 +1,5 @@
import { isNonEmptyArray } from 'twenty-shared/utils';
import { type PersonWorkspaceEntity } from 'src/modules/person/standard-objects/person.workspace-entity';
export const findPersonByPrimaryOrAdditionalEmail = ({
@@ -20,7 +22,7 @@ export const findPersonByPrimaryOrAdditionalEmail = ({
const personWithAdditionalEmail = people.find((person) => {
const additionalEmails = person.emails?.additionalEmails;
if (!Array.isArray(additionalEmails)) {
if (!isNonEmptyArray(additionalEmails)) {
return false;
}
@@ -45,8 +45,7 @@ export class MessageParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -82,8 +81,7 @@ export class MessageParticipantPersonListener {
const personEmails = personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.after.emails.primaryEmail,
...((eventPayload.properties.after.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.after.emails?.additionalEmails ?? []),
])
.filter(isDefined);
@@ -121,8 +119,8 @@ export class MessageParticipantPersonListener {
personEmails: personWithEmails
.flatMap((eventPayload) => [
eventPayload.properties.before.emails.primaryEmail,
...((eventPayload.properties.before.emails?.additionalEmails ??
[]) as string[]),
...(eventPayload.properties.before.emails?.additionalEmails ??
[]),
])
.filter(isDefined),
workspaceMemberIds: [],