In Practice, row polymorphism is more granular, allowing you to explicitly allow certain fields while tracking all other fields via a ("rest") type variable.
Example: PureScript allows you to remove specific fields from a record type. This feature, is called record subtraction, and it allows more flexibility when transforming or narrowing down records.
You can also apply exact field constraints; meaning, you can constrain records to have exactly the fields you specify.
Lastly, PureScript allows you to abstract over rows using higher-kinded types. You can create polymorphic functions that accept any record with a flexible set of fields and can transform or manipulate those fields in various ways. This level of abstraction is not possible in TypeScript.
These are just a few examples. In the most general sense, you can think of row polymorphism as a really robust tool that gives you a ton of flexibility regarding strictness and validation.
TypeScript does allow you to remove specific fields, if I understand you right [0]:
function removeField<T, K extends keyof T>(obj: T, field: K): Omit<T, K> {
const { [field]: _, ...rest } = obj;
return rest;
}
type Person = { name: string; age: number };
declare const p: Person;
const result = removeField(p, 'age'); // result is of type: Omit<Person, "age">
> PureScript allows you to abstract over rows using higher-kinded types. You can create polymorphic functions that accept any record with a flexible set of fields and can transform or manipulate those fields in various ways. This level of abstraction is not possible in TypeScript.Again, if I understand you correctly, then TypeScript is able to do fancy manipulations of arbitrary records [1]:
type StringToNumber<T> = {
[K in keyof T]: T[K] extends string ? number : T[K]
}
function stringToLength<T extends Record<string, unknown>>(obj: T): StringToNumber<T> {
const result: Record<string, unknown> = {};
for (const key in obj) {
result[key] = typeof obj[key] === 'string' ? obj[key].length : obj[key];
}
return result as StringToNumber<T>;
}
const data = {
name: "Alice",
age: 30,
city: "New York"
};
const lengths = stringToLength(data);
lengths.name // number
lengths.age // number
lengths.city // number
[0] https://www.typescriptlang.org/play/?#code/GYVwdgxgLglg9mABA...[1] https://www.typescriptlang.org/play/?#code/C4TwDgpgBAysBOBLA...
edit: provided links to TS playground
const r1: { a: number; b: number } = { a: 10, b: 20 };
const r2: { a: number } = r1;
const r3: { a: number; b: string } = { b: "hello", ...r2 };
console.log(r3.b) // typescript thinks it's a string, but actually it's a number
The problem in question can be "fixed" like this
const r1: { a: number; b: number } = { a: 10, b: 20 };
const r2 = r1 satisfies { a: number };
const r3: { a: number; b: string } = { b: "hello", ...r2 };
Now, TS would warn us that "'b' is specified more than once, so this usage will be overwritten". And if we remove b property -- "Type 'number' is not assignable to type 'string'"Another "fix" would be to avoid using spread operator and specify every property manually .
Both of these solutions are far from ideal, I agree.
---
I don't advocate TS in this thread though; I genuinely want to understand what makes row polymorphism different, and after reading several articles and harassing Claude Sonnet about it, I still didn't grasp what row polymorphism allows over what TS has.