Search code examples
javascripttypescriptstringtypes

How to write a Typescript type that applies a function to a string?


I am attempting to write a type that applies the pluralize library to a string. The purpose of this is to take a list of strings that contains my database tables, and convert them to the pluralized version, as that is how relations are defined in my schema (Ex: Student -> Students).

I am an utter novice to writing types, and Google AI gave me the naive solution:

type Pluralize<T extends string> = T extends `${infer S}sis`
    ? `${S}ses`
    : T extends `${infer S}y`
    ? `${S}ies`
    : T extends `${infer S}s`
    ? T
    : `${T}s`;

This solution works, but it fails to properly convert words like "day" to "days", and instead converts it to "daies". I could in theory add more rules to make it robust, but that seems like reinventing the wheel.

I want to write a type that, instead of simply replacing the ending, applies a function to the string. I attempted the solution:

import pluralize from "pluralize";

type Pluralize<T extends string> = T extends string ? pluralize(T) : never;

This solution is wrong, but I'm not well-versed in Typescript and I'm not sure how to fix it. Any advice on a direction to go would be greatly appreciated.


Solution

  • If by "function" you specifically mean a JavaScript function, then this is impossible in TypeScript. JavaScript code runs at runtime, and JavaScript code cannot simply be "lifted" to the type level to run at compile time. TypeScript's type syntax is effectively a different, largely declarative language, and to the extent that it resembles JavaScript, it's because they are usually analogous to each other, but not the same. You can write const x = 5; and type X = 5;, but there's no type X = 2 + 3; or type X = Math.sqrt(25) or anything like that. As a declarative language there's little sense of control flow as you'd find in an imperative language like JavaScript. There's nothing like if (X < Y) { type Z = 1; } or while (X < 10) { type X = X + 1 }. There are recursive conditional types, but these are more analogous to recursive functional programming implementations than JavaScript. So no matter what, you cannot take some JavaScript library and import its runtime functions into the type level, and then use it in a function.

    There is a longstanding open feature request at microsoft/TypeScript#41577 to allow people to write "functions" to define types. It's not clear how that could be implemented. TypeScript has a lot of features and moving parts, and any addition would have to play nicely with them.

    For example, there's type inference. You can write type X<T> = {x: T} and then declare function f<T>(x: X<T>): T and then call f({x: "abc"}) and TypeScript can infer that T must be string for that call. Meaning it looks at {x: "abc"}, and at the definition of X<T>, and figures out what T should have been for that to match up. It's like running a function backwards. Inference isn't perfect, but if people started adding arbitrary type functions, they'd have to at least think about inference.

    And types aren't one-to-one mappings to values, they are more like one-to-many. So you'd have to decide what your type does, for example, with unions and intersections. Does it distribute across them somehow? What about optional properties? Etc etc etc.


    Anyway, even if they did implement it, it's quite unlikely it would be something where you could just import some runtime library. At best it would be some more ergonomic way to write declarative types in a more imperative "style". But this is pure speculation. Suffice it to say that this is currently impossible.

    If you want some kind of type system behavior, you'll have to write it in the type system. That will look like indexed accesses or conditional types, and they can be very complex and have edge cases.