Chapter 3
How the standard records a biologic, its characterization, its assays, its process and its regulatory status: the TypeScript type model in types.ts, read field by field, with the units, gaps and defects an implementer has to know about.
Many data standards publish a formal schema language document (an XML Schema, a JSON Schema, an ASN.1 module). WIA-BIO-016 does not. Its specification (spec/WIA-BIO-016-v1.0.md) explains the science in prose and equations, and the only machine-readable definition of what a record looks like is the TypeScript module api/typescript/src/types.ts. It is worth saying this plainly at the start, because it determines how the rest of the book reads the standard:
types.ts (this chapter).BiopharmaSDK class and standalone functions in index.ts, plus the wia-bio-016 shell CLI (Chapter 4 onward).A TypeScript interface is a reasonable way to define a data format. It is precise about field names, optionality and enumerated values, and it can be checked mechanically by the compiler at every call site. It also has limits that a JSON Schema would not: TypeScript types disappear at run time, so a JSON document arriving from a laboratory instrument or another system is not validated by them; and a TypeScript number says nothing about units, ranges or precision. In WIA-BIO-016 the units live in comments next to each field. Those comments are therefore normative in practice, and this chapter treats them that way.
The model does not use one consistent convention. Molar concentrations are in mol/L (M), clearance is in L/h, temperatures are in degrees Celsius, stability durations are in months, and some fractions are expressed on a 0–1 scale while others are percentages on a 0–100 scale. None of this is wrong in itself, but mixing the conventions is the most common source of silent errors when records from different sources are combined. The table below collects the conventions stated in the source comments.
| Quantity | Where it appears | Unit or scale in types.ts | Implementer's note |
|---|---|---|---|
| Molecular mass | MolecularWeight.theoretical, .measured, .uncertainty | Da | DrugCandidate.molecularWeight is a bare number with no unit comment; assume Da. |
| Mass accuracy | MolecularWeight.accuracy | ppm | Store the signed or absolute error consistently; the type does not say which. |
| Binding concentrations, Kd | BindingAffinity.*Conc, BindingAffinityResult.kd, KineticParameters.kd, DrugCandidate.targetAffinity | M (mol/L) | 1 nM must be written 1e-9, not 1. |
| Rate constants | KineticParameters.kon / .koff | M−1s−1 / s−1 | complexHalfLife is therefore in seconds. |
| Dose | PKParameters.dose | mg | Weight-based doses (mg/kg) must be multiplied out first; patientWeight is recorded but not used. |
| Clearance / volume | PKParameters.clearance / .volumeOfDistribution | L/h / L | Antibody clearance is usually reported in L/day; divide by 24. |
| PK/PD concentrations | PKResult.c0, PDParameters.concentration, .ec50 | mg/L | Mass units here, molar units for Kd: the two cannot be compared without the molecular mass. |
| Temperature | BindingAffinity.temperature, StabilityConditions.temperature | °C | Convert to kelvin for any Arrhenius calculation. |
| Stability duration | StabilityConditions.duration, StabilityResult.predictedShelfLife | months | A month is not a fixed number of days; state the conversion when you compute it. |
| Fractions 0–1 | Glycan.abundance, PTM.occupancy, PKParameters.bioavailability, ADAData.incidenceRate, purification yield/purity, DrugCandidate.stability | dimensionless 0–1 | Never write 95 where 0.95 is meant. |
| Percentages 0–100 | QualityAttributes (except endotoxin, HCP, DNA), StabilityResult.potency/monomerContent/aggregates, CellCultureParams.viability | % | The same concept (purity) is 0–1 in PurificationProcess and 0–100 in QualityAttributes. |
| Impurities | QualityAttributes.endotoxin / .hcp / .residualDNA | EU/mg / ppm / pg per dose | Three different normalisation bases (per mass, per mass ratio, per dose). |
A practical rule follows: when you ingest data, convert everything to the unit named in the comment and keep the original value and unit alongside it in an extension field (see 3.7.4). The model has no place for the original unit, so if you do not keep it yourself, a later reviewer cannot tell whether a conversion happened.
convertConcentration()types.ts declares three unit types: ConcentrationUnit ('M' | 'mM' | 'μM' | 'nM' | 'pM' | 'mg/mL' | 'μg/mL'), TimeUnit (seconds to years) and TemperatureUnit (celsius, fahrenheit, kelvin). No interface field uses any of them; every field has a fixed unit instead. The only related code is the SDK method convertConcentration(value, fromUnit, toUnit, molecularWeight?), whose parameters are typed as plain string, not as ConcentrationUnit. Its behaviour is narrower than its signature suggests:
const sdk = new BiopharmaSDK();
sdk.convertConcentration(2.5, 'nM', 'M');
// 2.5e-9 (molar-to-molar conversions work)
sdk.convertConcentration(10, 'mg/mL', 'μM', 150000);
// throws BiopharmaError, code 'BIO007':
// "Unit conversion not implemented for these units"
// (mass units are declared but not converted; molecularWeight is ignored)
sdk.convertConcentration(2.5, 'µM', 'nM');
// also throws BIO007: the code matches U+03BC GREEK SMALL LETTER MU,
// while many keyboards and instruments emit U+00B5 MICRO SIGN
The last case is easy to miss. The string 'μM' in the source uses the Greek letter mu (U+03BC). Text exported from spreadsheets and instrument software frequently uses the micro sign (U+00B5), which looks identical on screen. An ingestion layer should normalise the micro sign to the Greek mu (Unicode NFKC normalisation does this) before calling the SDK, or better, should convert to molar values itself and never pass unit strings at all.
Five union types describe what kind of product a record is about. They are string literal unions, which serialise to JSON as plain strings and are case-sensitive: 'CHO' is not a valid ExpressionSystem, 'cho' is.
| Type | Allowed values | Comments |
|---|---|---|
DrugClass | monoclonal-antibody, antibody-fragment, fusion-protein, cytokine, enzyme, vaccine, antibody-drug-conjugate, peptide, recombinant-protein | Nine classes, one per product. Used only by DrugCandidate.drugType. |
AntibodyIsotype | IgG1, IgG2, IgG3, IgG4, IgM, IgA, IgE | Declared but not referenced by any interface. |
AntibodyFormat | full-igg, fab, scfv, diabody, bite, nanobody, dart | Declared but not referenced by any interface. |
VaccineType | mrna, viral-vector, protein-subunit, inactivated, live-attenuated, toxoid, conjugate | Declared but not referenced; no vaccine-specific record exists. |
ExpressionSystem | e-coli, yeast, cho, hek293, ns0, sp2-0, insect, plant | Used by DrugCandidate and CellCultureParams. |
The table exposes a structural point. The model has no root "product" entity that ties these facts together. DrugCandidate carries a drugType and an expressionSystem, but there is no field in which to say that the candidate is an IgG1 in full-IgG format, and no field for a product identifier at all. An application that stores more than one product must therefore add its own identity layer. A minimal version is shown below; it only combines types that the standard already defines.
import type {
DrugClass, AntibodyIsotype, AntibodyFormat, ExpressionSystem,
} from '@wia/bio-016/types'; // path per package.json "exports"
/** Application-level identity record (not part of WIA-BIO-016 v1.0). */
interface ProductIdentity {
productId: string; // your own stable key
drugClass: DrugClass;
isotype?: AntibodyIsotype; // only for antibody-based classes
format?: AntibodyFormat;
expressionSystem: ExpressionSystem;
}
Two consistency rules are worth enforcing in that layer, because the types alone cannot. First, isotype and format only make sense when drugClass is monoclonal-antibody, antibody-fragment or antibody-drug-conjugate (and, for an Fc-fusion protein, the Fc isotype can be informative). Second, the class and the format overlap: a Fab can be recorded either as class antibody-fragment or as class monoclonal-antibody with format fab. Pick one convention and document it; otherwise queries such as "all fragments" will silently miss records.
Note also what the identity types cannot express. There is no field for an antibody-drug conjugate's linker, payload or drug-to-antibody ratio, even though the specification (§2.2) discusses them; a bispecific that is not a BiTE or DART has no format value; and cell and gene therapy products fall outside the enumerations. Chapter 8 returns to these gaps and proposes additions.
The characterization group describes the molecule itself: its primary sequence, its mass, its charge, its glycans and its other modifications. The central interface is ProteinCharacterization:
interface ProteinCharacterization {
sequence: string; // amino acid sequence
molecularWeight: MolecularWeight;
isoelectricPoint: number;
extinctionCoefficient: number; // M^-1 cm^-1
glycosylationSites?: number[];
glycans?: Glycan[];
ptms?: PTM[];
disulfideBonds?: Array<[number, number]>;
}
The most consequential design choice here is that sequence is a single string. A monoclonal antibody is a heterotetramer of two heavy and two light chains, and every position-based field in the model (glycosylationSites, PTM.position, the pairs in disulfideBonds) is a bare integer. Without a chain identifier and a numbering scheme, "position 297" is ambiguous: it could be a residue index in the concatenated string, a position in the heavy chain counted from 1, or the EU-numbered Asn297 that everyone in the field means when they talk about the conserved Fc glycosylation site.
An IgG1 illustrates the scale of the problem. It has sixteen disulfide bonds: twelve intrachain bonds (one in each immunoglobulin domain) and four interchain bonds (two heavy-heavy bonds in the hinge and one heavy-light bond on each arm). An inter-chain bond cannot be written as two positions in one string unless the reader knows how the chains were concatenated and in what order. Until the standard adds chain-qualified positions, a conforming implementation should do three things: store the sequence as a documented concatenation (for example heavy chain, then light chain, one copy each, with no separator), state the numbering scheme in PTM.details or an extension field, and keep a chain-qualified copy of each site in an extension field so that it survives export.
MolecularWeightMolecularWeight pairs a theoretical value (calculated from the sequence plus expected modifications) with a measured value, both in daltons. method is one of mass-spec, sec-mals, sds-page or calculated. The optional fields are uncertainty (in Da) and accuracy (in ppm). For a mass-spectrometry result, the accuracy is the relative mass error: (measured − theoretical) / theoretical × 106. For example, a theoretical 148,220 Da and a measured 148,224 Da give an error of about 27 ppm. For SEC-MALS or SDS-PAGE the ppm field is meaningless and should be omitted rather than filled with a computed number that suggests precision the method does not have.
Two things are not recorded: which glycoform the theoretical mass refers to (an intact IgG is a mixture of glycoforms, so "the" intact mass is usually the mass of one named species such as G0F/G0F), and whether the sample was deglycosylated or reduced before measurement. Both belong in an extension field.
GlycanEach Glycan entry has a type (n-glycan or o-glycan), a free-text composition label such as G0F, a relative abundance on the 0–1 scale, an optional count of sialicAcids, and two booleans, fucosylated and highMannose. The booleans make the two most-queried properties explicit instead of forcing every consumer to parse the composition label, which is sensible: afucosylated species matter for antibody-dependent cellular cytotoxicity and high-mannose species can affect clearance, as Chapter 2 explains.
Because composition is free text, two laboratories can label the same structure differently (Man5 versus M5, for instance). A controlled vocabulary for the label, published alongside the implementation, is the cheapest fix. An invariant is also worth checking at ingestion: within one analysis, the sum of abundance values must not exceed 1. It may be below 1 if minor species were not reported, but a sum above 1 means either a scale error (percentages entered as fractions) or double counting.
PTMPTM records one modification at one position on one residue, with optional details and an occupancy fraction. The type union allows only glycosylation, phosphorylation, acetylation, methylation and oxidation. For therapeutic antibodies this list omits several of the modifications that most often become critical quality attributes: asparagine deamidation, aspartate isomerization, N-terminal pyroglutamate formation, C-terminal lysine variants and glycation. None of these can be recorded without an extension, which is a real limitation for anyone trying to capture a peptide-mapping report in this format. Section 3.7.4 shows one way to add them without breaking consumers that only understand the original union.
The assay group holds inputs and outputs of the calculations described in Chapters 4 and 5. A pattern repeats across it: for each calculation there is a parameters interface (what you measured or assumed) and a result interface (what the SDK computes). Only some of these are meant to be stored as primary data; others are derived values that should be recomputable from the stored inputs.
BindingAffinity, BindingAffinityResult, KineticParametersBindingAffinity holds equilibrium concentrations of antibody, antigen and complex (all in M), the temperature in °C and optional pH and buffer. BindingAffinityResult holds the derived kd, the free concentrations, a bindingStrength class, fractionBound and a boolean therapeuticSuitability. The classification thresholds and their disagreement with the specification's own table are the subject of Chapter 4; here the concern is storage. Two details affect serialisation:
complexConc is 0, the SDK sets kd to JavaScript Infinity. JSON has no representation for infinity, and JSON.stringify writes null. A consumer that reads "kd": null cannot tell "no binding detected" from "value missing". Store an explicit flag or omit the result.KineticParameters stores kon, koff, the derived kd and complexHalfLife (seconds), a method (spr, bli, ifa, elisa or kinexA) and a temperature. The SDK's calculateKinetics(kon, koff) returns this interface, but it has no way of knowing how the constants were measured, so it always fills method: 'spr' and temperature: 25:
const k = sdk.calculateKinetics(5.0e5, 3.0e-4);
// { kon: 500000, koff: 0.0003, kd: 6e-10,
// complexHalfLife: 2310.49..., method: 'spr', temperature: 25 }
// Overwrite the placeholders with what was actually done:
const record: KineticParameters = { ...k, method: 'bli', temperature: 30 };
If the placeholder values are stored unchanged, every kinetic record in the database will claim to be a 25 °C SPR measurement. That is a data-integrity problem, not a calculation problem, and it is easy to prevent with the spread-and-overwrite pattern above.
PKParameters, PKResult, PDParameters, PDResultPKParameters records a dose (mg), bioavailability (0–1), clearance (L/h), volumeOfDistribution (L), a route (iv, sc, im, oral or inhalation) and an optional patientWeight (kg). PKResult has auc (mg·h/L), halfLife (h), c0 (mg/L), ke (h−1), a dosingInterval (h) and two optional fields, cmax and tmax, which the SDK never fills.
The unit choice matters more for antibodies than for small molecules. Antibody clearance is conventionally reported per day. To store a clearance of 0.2 L/day, write 0.2 / 24, that is about 0.00833 L/h. With a volume of 6 L (an illustrative value), the resulting half-life is 0.693 × 6 / 0.00833 ≈ 499 h, about 21 days, which is the order of magnitude expected for an IgG. Entering 0.2 without conversion would produce a half-life 24 times too short, and nothing in the type would flag it. The route and the patient weight are stored but do not enter the calculation, so a subcutaneous dose must be recorded with its bioavailability explicitly, and weight-based dosing must be converted to milligrams before the record is created.
PDParameters takes a concentration and ec50 in mg/L, an emax, and optional hillCoefficient and baseline. Because Kd elsewhere in the model is molar, a record that holds both a binding result and a PD result for the same molecule contains the same kind of quantity in two unit systems. Converting between them needs the molecular mass (for an IgG of about 150 kDa, 1 mg/L corresponds to roughly 6.7 nM), which is one more reason to keep MolecularWeight in every product record.
Two immunogenicity interfaces look similar but have different status, and they should be stored differently. ADAData is an observation from a study: an incidenceRate (0–1), an optional titer, a neutralizing flag, an optional timeToADA (days) and a clinicalImpact category. ImmunogenicityParams (adaRate, severity 1–10, durationFactor, toleranceFactor, priorData) are the inputs to the WIA immunogenicity risk score, a triage heuristic discussed in Chapter 5. Severity, duration and tolerance factors are judgements, not measurements. A database should mark such records as assessments, record who made them, and never mix them with study results in the same table. ImmunogenicityRisk, the output, is likewise derived and recomputable.
StabilityConditions and StabilityResultStabilityConditions names a storage condition: temperature (°C), optional humidity (% RH), duration (months), a type (long-term, accelerated, stress or real-time) and an optional lightExposed flag. StabilityResult embeds one condition and reports potency (%), monomerContent (%), aggregates (%), pH, an appearance category (clear, slightly-opalescent, opalescent, turbid, precipitate), a boolean passesSpec and an optional predictedShelfLife (months).
Each StabilityResult is one pull point. A stability study is a series of pull points per condition, per batch, and the model has no study or batch identifier to group them. It also has no field for fragments or charge variants at a time point, even though both are routinely trended, and no record of the specification against which passesSpec was judged. A sound implementation keeps a study table (study ID, batch, condition) and stores each StabilityResult with a foreign key and a time-point value, which is more precise than inferring the time point from duration.
QualityAttributesQualityAttributes is the release-style snapshot used, among other places, by assessBiosimilarity(). It holds purity, monomer, aggregates and fragments (all %), the charge-variant distribution mainPeak, acidicVariants and basicVariants (%), potency (% of reference), endotoxin (EU/mg), and optional hcp (ppm) and residualDNA (pg/dose).
Two checks are useful at ingestion. The size-variant triple monomer + aggregates + fragments should add up to about 100 when all three come from the same SEC run, and so should mainPeak + acidicVariants + basicVariants from the same charge-variant run. Both sums drifting away from 100 usually indicate a mixed-source record. The second point is semantic: purity is commented "Purity by SEC (%)", which makes it numerically the same as monomer in most data sets. Chapter 2 explains why purity should be an orthogonal statement; in the data model, record which method produced purity in an extension field so the redundancy is visible rather than hidden.
CellCultureParams and PurificationProcessCellCultureParams records a cellLine name, an expressionSystem, productivity, cultureDuration (days), viability (%) and cellDensity (cells/mL). The comment on productivity says "Volumetric productivity (mg/L)". In bioprocess usage, mg/L is a titer (product concentration at harvest), while volumetric productivity is a rate (for example mg/L/day). Store the harvest titer in this field, as the unit implies, and put a rate in an extension if you need one. The record also has no batch or run identifier and no time series, so it describes a culture summary, not a process history.
PurificationProcess is a list of steps, each with a name, a type (affinity, ion-exchange, hydrophobic, size-exclusion or ultrafiltration), a step yield and purity on the 0–1 scale, followed by overallYield, finalPurity and processDuration (hours). Two observations:
overallYield equals the product of the step yields. The type does not enforce this, and nothing in the SDK checks it. A validator should recompute the product and flag records where the stored value differs beyond rounding.type union covers chromatography and ultrafiltration only. Low-pH viral inactivation and virus filtration, the dedicated clearance steps that ICH Q5A evaluates, have no value. Recording them as ultrafiltration would be misleading. Until the union is extended, keep them in an extension list rather than forcing them into a wrong category.DrugCandidate and DrugValidationResultDrugCandidate is the summary record that validateDrugCandidate() checks (Chapter 6). It restates several facts that exist elsewhere in the model in a flattened form: targetAffinity is a Kd in M, molecularWeight is a bare number, stability is a 0–1 score, and immunogenicityRisk and manufacturingComplexity are three-level categories. Its developmentStage uses its own list (discovery, preclinical, phase1–phase3, approved), which differs from the separate ClinicalPhase type (preclinical, phase1–phase4). Mapping between them needs a rule for discovery and for approved versus phase4. The output interface DrugValidationResult includes estimatedTimeline, estimatedCost and successProbability; as Chapter 6 shows, these are fixed guesses in code and should not be persisted as if they were analysis results.
RegulatorySubmissionRegulatorySubmission records a pathway, a submission type (ind, bla, maa, amendment or annual-report), an indication, an optional phase, optional estimatedSubmission and estimatedApproval dates, and booleans for orphanDrug and fastTrack. The RegulatoryPathway union is:
type RegulatoryPathway =
| 'fda-bla' | 'ema-maa' | 'pmda-japan' | 'nmpa-china'
| 'biosimilar-351k' | 'accelerated' | 'breakthrough';
This list mixes three different kinds of thing. fda-bla, ema-maa, pmda-japan and nmpa-china identify an authority and its application route. biosimilar-351k is a specific US statutory route (section 351(k) of the Public Health Service Act), whereas an EU biosimilar is still filed as an MAA. accelerated and breakthrough are expedited programmes that apply on top of an application, not alternatives to it, and the model already has a separate fastTrack boolean for a third programme of the same kind. There is also no value for Korea's Ministry of Food and Drug Safety (MFDS), Health Canada or other authorities. A cleaner model separates the authority, the application route and a list of designations; Chapter 7 uses that separation when mapping the model to CTD modules. In the meantime, do not use accelerated or breakthrough as the pathway of a record whose authority matters, because the authority is then lost.
The two date fields are typed as JavaScript Date. That is fine in memory but has consequences on the wire, covered in 3.7.3.
BIOPHARMA_CONSTANTStypes.ts exports a frozen object of constants. Some are physical constants; most are typical values or policy thresholds used by the SDK. The distinction matters, because a physical constant is not negotiable while a threshold is a design decision that an implementer may need to override.
| Name | Value in code | Unit | Kind | Remark |
|---|---|---|---|---|
AVOGADRO | 6.022e23 | mol−1 | physical | Rounded value. |
GAS_CONSTANT | 1.987 | cal/(mol·K) | physical | Correct in calories, but predictShelfLife() uses its own local R = 8.314 J/(mol·K). Both are the same constant in different units. |
BOLTZMANN | 1.381e-23 | J/K | physical | Not used by the SDK. |
STANDARD_TEMP | 298.15 | K | convention | 25 °C. |
IGG_MW | 150000 | Da | typical value | Approximate; actual IgG masses vary by sequence and glycoform. |
IGG_HALF_LIFE | 21 | days | typical value | Order of magnitude for IgG1, IgG2 and IgG4; IgG3 is shorter. |
FCRN_KD | 200e-9 | M | typical value | A single point; the specification (§10.4) gives a range at pH 6.0. |
PLASMA_VOLUME | 3.5 | L | typical value | Adult reference; the specification gives a range. |
MIN_THERAPEUTIC_KD | 10e-9 | M | WIA threshold | 10e-9 is 1×10−8 M, i.e. 10 nM. Easy to misread as 1 nM. |
MAX_AGGREGATES | 5 | % | WIA threshold | Example specification limit, product-specific in practice. |
MIN_MONOMER | 95 | % | WIA threshold | As above. |
MAX_ENDOTOXIN | 1 | EU/mg | WIA threshold | As above; actual limits depend on dose and route. |
MIN_SHELF_LIFE | 24 | months at 2–8 °C | WIA threshold | A target, not a regulatory requirement. |
The gas constant deserves one more sentence because it is a classic trap. In the Arrhenius term exp(−Ea/RT), Ea and R must be in the same energy unit. If an activation energy in J/mol is combined with BIOPHARMA_CONSTANTS.GAS_CONSTANT (calories), the exponent is wrong by a factor of about 4.184. When you write your own code against this model, name the unit in the variable (R_J_PER_MOL_K) instead of importing the constant by its generic name.
Errors are represented by BiopharmaError, a subclass of Error with a code from the BiopharmaErrorCode enumeration and an optional details object. Ten codes are defined:
| Code | Enum member | Intended meaning | Thrown by the v1.0 SDK? |
|---|---|---|---|
| BIO001 | INVALID_AFFINITY | Affinity value unusable or out of range | No |
| BIO002 | INSUFFICIENT_STABILITY | Stability below requirement | No |
| BIO003 | HIGH_IMMUNOGENICITY | Immunogenicity risk unacceptable | No |
| BIO004 | POOR_PK_PROPERTIES | PK profile unsuitable | No |
| BIO005 | QUALITY_FAILURE | Quality attribute out of specification | No |
| BIO006 | REGULATORY_NONCOMPLIANCE | Regulatory requirement not met | No |
| BIO007 | INVALID_PARAMETERS | Input outside the allowed domain | Yes: every thrown error in index.ts |
| BIO008 | MANUFACTURING_FAILURE | Process step failed | No |
| BIO009 | BIOSIMILARITY_FAILURE | Comparison not similar | No |
| BIO010 | SAFETY_CONCERN | Safety signal | No |
In v1.0 only BIO007 is ever raised. Domain outcomes such as a weak affinity or a failed biosimilarity comparison are reported as values (warnings, a conclusion string, a boolean), not as errors. That is a defensible choice, since a candidate with poor affinity is a valid input that deserves a valid answer, but it means the other nine codes are reserved vocabulary. An implementation may use them for its own validations, and conformance tests should not expect the SDK to raise them.
One serialisation detail: JSON.stringify on a BiopharmaError yields {"code":"BIO007","details":{...},"name":"BiopharmaError"} without the message, because message is not an own enumerable property of an Error. Any service that returns errors as JSON must copy the message explicitly.
Result<T, E>types.ts also defines a discriminated union for operations that can fail, and an asynchronous variant:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
type AsyncResult<T, E = Error> = Promise<Result<T, E>>;
No SDK function returns it; they all throw. If your application prefers values to exceptions, for example at a service boundary where every response must be JSON, wrap the calls once:
import { BiopharmaError, type Result } from '@wia/bio-016';
function attempt<T>(fn: () => T): Result<T, { code: string; message: string }> {
try {
return { success: true, data: fn() };
} catch (e) {
if (e instanceof BiopharmaError) {
return { success: false, error: { code: e.code, message: e.message } };
}
throw e; // programming errors should not be turned into data
}
}
const r = attempt(() => sdk.calculatePharmacokinetics(pk));
if (!r.success) console.error(r.error.code, r.error.message);
Implementers who build from source will meet one more defect before any of the above. Both files end with export blocks that re-export names already exported at their declarations: types.ts repeats every interface and type in a closing export type { ... } list and repeats BIOPHARMA_CONSTANTS, BiopharmaErrorCode and BiopharmaError in an export { ... }; index.ts declares export class BiopharmaSDK and later adds export { BiopharmaSDK }. Checked with a current TypeScript compiler, this produces errors TS2323 ("Cannot redeclare exported variable") and TS2484 ("Export declaration conflicts with exported declaration") for every repeated name, so the package's typecheck script (tsc --noEmit) fails. The fix is mechanical: delete the trailing re-export blocks, keeping the export * from './types' line in index.ts. This book's examples assume that fix has been applied.
The standard defines record types but not how they connect. The diagram below shows the relationships an application needs, following the order of the development workflow. Solid arrows are relations that the types can express by nesting; dashed arrows are relations that need an identifier the model does not provide.
ProductIdentity (application layer: id, DrugClass, format, ExpressionSystem)
|
+--> ProteinCharacterization --> MolecularWeight
| +--> Glycan[] +--> PTM[] +--> disulfideBonds
|
+- - > assays
| BindingAffinity --> BindingAffinityResult (derived)
| KineticParameters (kd derived)
| PKParameters --> PKResult (derived)
| PDParameters --> PDResult (derived)
| ADAData (observed) ImmunogenicityParams --> ImmunogenicityRisk
|
+- - > process: CellCultureParams, PurificationProcess
|
+- - > batch - - > QualityAttributes
| +- - > stability study --> StabilityResult[] --> StabilityConditions
|
+- - > RegulatorySubmission[]
DrugCandidate: flattened summary of several of the above (for gating)
The document below is a worked example for a hypothetical IgG1 antibody called "EX-01". Every number is illustrative and chosen only to be internally consistent; none describes a real product. The envelope fields (wia, schemaVersion, product, ext) are a convention proposed in this book, not part of WIA-BIO-016 v1.0, which defines only the inner records. The sequence is shortened for print; a real record carries the full concatenated sequence described in 3.3.1.
{
"wia": "WIA-BIO-016",
"schemaVersion": "1.0.0",
"product": {
"productId": "EX-01",
"drugClass": "monoclonal-antibody",
"isotype": "IgG1",
"format": "full-igg",
"expressionSystem": "cho"
},
"characterization": {
"sequence": "EVQLVESGGGLVQPGGSLRLSCAAS",
"molecularWeight": {
"theoretical": 148220,
"measured": 148224,
"method": "mass-spec",
"accuracy": 27
},
"isoelectricPoint": 8.4,
"extinctionCoefficient": 210000,
"glycosylationSites": [297],
"glycans": [
{ "type": "n-glycan", "composition": "G0F", "abundance": 0.42, "fucosylated": true, "highMannose": false },
{ "type": "n-glycan", "composition": "G1F", "abundance": 0.38, "fucosylated": true, "highMannose": false },
{ "type": "n-glycan", "composition": "G2F", "abundance": 0.10, "fucosylated": true, "highMannose": false },
{ "type": "n-glycan", "composition": "Man5", "abundance": 0.04, "fucosylated": false, "highMannose": true },
{ "type": "n-glycan", "composition": "G0", "abundance": 0.03, "fucosylated": false, "highMannose": false }
],
"ptms": [
{ "type": "oxidation", "position": 252, "residue": "M",
"details": "heavy chain, EU numbering", "occupancy": 0.03 }
]
},
"kinetics": {
"kon": 500000, "koff": 0.0003, "kd": 6e-10,
"complexHalfLife": 2310.49, "method": "bli", "temperature": 30
},
"pk": {
"parameters": {
"dose": 700, "bioavailability": 1,
"clearance": 0.00833, "volumeOfDistribution": 6,
"route": "iv", "patientWeight": 70
}
},
"quality": {
"purity": 99.1, "monomer": 99.1, "aggregates": 0.7, "fragments": 0.2,
"mainPeak": 68.0, "acidicVariants": 22.0, "basicVariants": 10.0,
"potency": 102, "endotoxin": 0.05, "hcp": 12, "residualDNA": 5
},
"stability": [
{
"conditions": { "temperature": 5, "duration": 24, "type": "long-term" },
"potency": 98, "monomerContent": 98.9, "aggregates": 0.9,
"pH": 6.0, "appearance": "clear", "passesSpec": true
}
],
"submissions": [
{ "pathway": "fda-bla", "type": "ind", "indication": "example indication",
"phase": "phase1", "estimatedSubmission": "2027-03-31T00:00:00.000Z" }
],
"ext": {
"org.example.lims": {
"sitesChainQualified": [{ "chain": "HC", "scheme": "EU", "position": 297 }],
"clearanceSource": { "value": 0.2, "unit": "L/day" }
}
}
}
A reader can check the example against the rules in this chapter. The glycan abundances add up to 0.97, not more than 1. Size variants add up to 100.0 (99.1 + 0.7 + 0.2) and charge variants to 100.0. The kinetic Kd equals koff/kon. The clearance was converted from 0.2 L/day, and the original value is kept under ext. The PK result is not stored at all, because it can be recomputed from the parameters; storing it would create a second source of truth that could drift out of date if the SDK changes.
Infinity and NaN become null in JSON. Omit the field or add an explicit status instead.JSON.stringify turns a Date into a UTC string such as "2027-03-31T00:00:00.000Z", but JSON.parse does not turn it back. Revive date fields explicitly after parsing, or the object will not match the type at run time. If only the day matters, store "2027-03-31" and say so, to avoid time-zone shifts.disulfideBonds is serialised as [[22, 96], ...]; validate that every inner array has exactly two integers.IgG1) and kinexA. Reject unknown values instead of mapping them to the nearest match.Because TypeScript types vanish at run time, the rules have to be enforced by an explicit validator at every point where JSON enters the system. A JSON Schema generated from types.ts (several open-source generators exist) is a practical starting point, with the unit and range rules above added by hand, since they are not in the types.
WIA-BIO-016 v1.0 is a calculation-oriented model. Several things that a regulated laboratory or manufacturing system must record are outside its scope:
method enumerations, there is no reference to an analytical procedure, its version, the instrument, or replicate values and their variability.passesSpec and the threshold constants exist, but there is no record of the specification itself, so a pass or fail cannot be re-judged later.Extensions should follow two principles: never change the meaning of a standard field, and keep additions in a namespace that consumers can safely ignore. In TypeScript that means extending interfaces instead of editing them:
import type { PTM, StabilityResult } from '@wia/bio-016/types';
/** Adds modifications missing from PTM['type'] without widening the original. */
interface PTMExt extends Omit<PTM, 'type'> {
type: PTM['type'] | 'deamidation' | 'isomerization'
| 'pyroglutamate' | 'c-terminal-lysine' | 'glycation';
chain?: 'HC' | 'LC';
numbering?: 'EU' | 'Kabat' | 'sequential';
}
/** Ties a pull point to its study, batch and time point. */
interface StabilityPullPoint extends StabilityResult {
studyId: string;
batchId: string;
timePointMonths: number;
fragments?: number; // %
}
/** When exporting to a strict v1.0 consumer, map extended PTMs down. */
function toCorePTM(p: PTMExt): PTM | null {
const core: PTM['type'][] =
['glycosylation', 'phosphorylation', 'acetylation', 'methylation', 'oxidation'];
return core.includes(p.type as PTM['type'])
? { type: p.type as PTM['type'], position: p.position, residue: p.residue,
details: p.details, occupancy: p.occupancy }
: null; // keep it under "ext" instead of losing it silently
}
The down-mapping function makes the loss visible: a deamidation site that a v1.0 consumer cannot represent is returned as null, and the caller decides to keep it under the ext namespace rather than dropping it. That is the general pattern for the whole model: be strict about what the standard defines, explicit about what it does not, and never let a conversion hide information.
WIA-BIO-016 defines its data format through TypeScript types, with units carried in comments. The types cover product identity, characterization, binding, PK/PD, immunogenicity, stability, quality, process and regulatory records, but they have no product root, no chain-qualified positions, no batch or study identifiers and no audit trail. Units mix molar and mass concentrations and 0–1 and 0–100 scales. The SDK throws only BIO007, returns placeholder method and temperature values from calculateKinetics(), produces Infinity for an unbound complex, and the source as distributed needs its duplicate export blocks removed before it type-checks. A careful implementation converts to base units at ingestion, validates invariants the types cannot express, keeps provenance in namespaced extensions and stores inputs rather than derived outputs. Chapter 4 now puts these records to work in the binding and PK/PD calculations.