Problem
FileNameCell guards its citation line on a value derived from the citation rather than on the citation itself, so TypeScript's narrowing does not reach the string that is actually rendered.
// components/.../SourceDatasets/components/MainColumn/components/table/components/FileNameCell/fileNameCell.tsx
const citation = publicationString
? splitTrailingWord(publicationString)
: null;
…
{citation && ( // citation → { head, tail } narrowed
…
{doi ? <Link label={…citation.head…citation.tail…} />
: publicationString } // publicationString → string | null NOT narrowed
)}
The relevant types:
publicationString: string | null (@types/network.ts:247)
splitTrailingWord(text: string): { head: string; tail: string } — never returns null
- so
citation: { head: string; tail: string } | null
Narrowing follows the expression that is tested. Testing citation tells TypeScript about citation; nothing in the type system records that citation was derived from publicationString, so inside the block publicationString remains string | null even though it provably cannot be null there.
Demonstrated. Changing the no-DOI branch from publicationString to publicationString.trimEnd() produces:
error TS18047: 'publicationString' is possibly 'null'.
Why nothing is broken today
The no-DOI branch renders publicationString as a JSX child, and null is a valid ReactNode — it renders nothing. So string | null satisfies the type and the weaker knowledge never bites.
Why it is still worth fixing
- It taxes the next edit. Anyone needing to use that string in that branch — uppercase it, measure it, pass it to a function taking
string — hits TS18047 and has to add ! or ?? "", silencing a check that was never genuinely in doubt.
- The condition states a slightly different fact than it means.
citation && reads as "if we produced a split citation"; the real condition is "if there is a citation string". Both spellings of one fact sit in the same block, and the no-DOI branch renders the original while the guard tested the derivative.
Root cause and suggested fix
splitTrailingWord is only needed in the DOI branch — it exists to hold the external-link icon against the citation's last word, and there is no icon without a DOI. Hoisting the split above the doi check is what forced the guard onto the derived value.
Guard on publicationString and split inside the DOI branch:
{publicationString && ( // publicationString → string
…
{doi ? <CitationLink citation={splitTrailingWord(publicationString)} doi={doi} />
: publicationString }
)}
Note this is not free: keeping it to a single splitTrailingWord call while splitting inside a JSX ternary needs either a small subcomponent (as sketched above) or accepting one inline call in the branch. Reverting to two calls would undo #3213, so whichever shape is chosen should keep it at one.
Acceptance criteria
Notes
Raised as a non-blocking nit on #3213 ("guarding on citation is a slightly indirect stand-in for 'we have a publication string' … narrowing is lost"), which is merged. Best folded into whichever ticket next touches FileNameCell rather than done on its own.
Compare rendered output by content rather than file hashes - this build is not byte-deterministic between runs.
Problem
FileNameCellguards its citation line on a value derived from the citation rather than on the citation itself, so TypeScript's narrowing does not reach the string that is actually rendered.The relevant types:
publicationString: string | null(@types/network.ts:247)splitTrailingWord(text: string): { head: string; tail: string }— never returns nullcitation: { head: string; tail: string } | nullNarrowing follows the expression that is tested. Testing
citationtells TypeScript aboutcitation; nothing in the type system records thatcitationwas derived frompublicationString, so inside the blockpublicationStringremainsstring | nulleven though it provably cannot be null there.Demonstrated. Changing the no-DOI branch from
publicationStringtopublicationString.trimEnd()produces:Why nothing is broken today
The no-DOI branch renders
publicationStringas a JSX child, andnullis a validReactNode— it renders nothing. Sostring | nullsatisfies the type and the weaker knowledge never bites.Why it is still worth fixing
string— hitsTS18047and has to add!or?? "", silencing a check that was never genuinely in doubt.citation &&reads as "if we produced a split citation"; the real condition is "if there is a citation string". Both spellings of one fact sit in the same block, and the no-DOI branch renders the original while the guard tested the derivative.Root cause and suggested fix
splitTrailingWordis only needed in the DOI branch — it exists to hold the external-link icon against the citation's last word, and there is no icon without a DOI. Hoisting the split above thedoicheck is what forced the guard onto the derived value.Guard on
publicationStringand split inside the DOI branch:Note this is not free: keeping it to a single
splitTrailingWordcall while splitting inside a JSX ternary needs either a small subcomponent (as sketched above) or accepting one inline call in the branch. Reverting to two calls would undo #3213, so whichever shape is chosen should keep it at one.Acceptance criteria
publicationString, and inside that blockpublicationStringis typedstring- verified by using it as a string (e.g..trimEnd()) without a null assertion and withtscclean.splitTrailingWordis still called at most once per render.ink.light/body-small-400, icon inside the anchor, final word and icon held on one line,(opens in a new tab)in the accessible name.npm run lint,npm run check-format,npx tsc --noEmitandnpm run build-prod:data-portalall pass.Notes
Raised as a non-blocking nit on #3213 ("guarding on
citationis a slightly indirect stand-in for 'we have a publication string' … narrowing is lost"), which is merged. Best folded into whichever ticket next touchesFileNameCellrather than done on its own.Compare rendered output by content rather than file hashes - this build is not byte-deterministic between runs.