15

In a project that my team is working on, we have type aliases for primitive types. The reason, I got from my team, for this is to have consistency across the codebase.

For example:

type FruitName = string;

const fruits: FruitName[] = ['apple', 'banana']

Now, because of this, if I use alias in a function argument, and when I hover on it, my IDE shows that the type is FruitName. It gives a feeling that maybe FruitName is an object instead of a string.

Can this be considered as an antipattern or is this a valid use case?

3
  • 7
    Note TypeScript is structurally, not nominally, typed, so you can still do e.g. type AnimalName = string; animal: AnimalName = 'monkey'; fruits.push(animal).
    – jonrsharpe
    Commented Mar 25, 2022 at 7:36
  • 2
    Haskell does this in its own standard library. FilePath is aliased to String; any function that expects a filename takes a FilePath as argument rather than a String. Provides no compile-time guarantees but makes the documentation cleaner, especially for things like writeFile which take two strings. One look at the type signature for that function makes it clear which string is the filename and which is the contents. Commented Mar 26, 2022 at 18:52
  • 1
    Sounds like poor man's microtyping
    – crizzis
    Commented Mar 27, 2022 at 18:52

6 Answers 6

20

By aliasing a simple type like that, you don't really gain anything on a compiler/type checking level. You can, obviously, do it. But that is mostly because TypeScript is very flexible with how types can be defined, composed, narrowed and aliased, many of which are useful.

For example, you could define type FruitName as:

type FruitName = "apple"|"banana"|"peach"|"lime" //todo add more fruit names

... etc., to define the possible string values that a field with type FruitName could have, which would make

const fruits: FruitName[] = ['apple', 'banana']

valid, but

const fruits: FruitName[] = ['apple', 'dog']

would not be valid, because "dog" would not be in the union of possible strings for that type.

So while the typeof for every element of type FruitName would still be string, the (TypeScript) type would be more helpful than a plain primitive alias. The alias as used in your example carries some use in that it tells the developer that the string should be a fruit, but the type contains no validation of that rule, which is the point of a type system. Well named variables and/or fields would probably do a much better job of informing the developer of such expectations.

No sane developer would do

fruits.push('dog')

because a dog is, obviously, not a fruit. And the variable name would inform the developer of what that array is intended for.

So... would a primitive alias like that be an antipattern? Soft, yes, because you are not really using the type system given to you in an effective way to enforce the rules that they imply. But it probably doesn't cause any actual harm.

3
  • 18
    Note that in many cases you might use an alias like the one provided by the OP and refine/change it later on... So right now it just so happens that FriutName is just a string, but maybe in the future it will become its own object type. By using an alias from the beginning in many places you don't have to change anything when switching the underlying type, if you use an explicit string you have to change the type everywhere, even in functions that simply pass the values around without "introspecting" them
    – GACy20
    Commented Mar 25, 2022 at 14:48
  • 1
    Yeah, similar to how constants allow you to edit 1 value instead of combing through all your code for magic numbers/strings. It all depends on what op's situation really is. Commented Mar 26, 2022 at 17:47
  • Perhaps I'd point one slight harm: dev UX. Multiply the number of aliases per variables and it gets annoying to enter it every single time you need to do something.
    – Alex
    Commented Mar 27, 2022 at 13:21
21

The reason for having a symbolic name for a type isn't "consistency". (Using string for everything would be even more consistent!) It's to increase the expressivity of your code.

Any system that deals with fruit or any other type that can be implemented by using a string will almost certainly also contain other types that can be implemented similarly. It is a good idea to distinguish e.g. between fruit and ice cream flavors because this allows you to see at a glance that a method expects a type of fruit (and not an ice cream flavor).

This improves the code base even though the resulting code is identical, because it makes it easier to work with for humans. (Developer time is incomparably more expensive than processor time, so it almost always makes sense to give the compiler more work to do to save thinking effort for people.)

Whether or not the type is technically an object should not be something that you have to reason about when developing - it is below the level of analysis that is necessary to work with a well-designed module. The important thing is that you make it easy for future users to get the fruit where they belong, and not accidentally use something else just because it's technically also a string.

3
  • This answer does not address the relevant point: does doing this make it less likely to accidentally use something else just because it's also a string? I'm not convinced this approach would ever help either the compiler or the human get it right. Commented Mar 25, 2022 at 18:32
  • I would add another point: imagine fruitnames being used at multiple points in a complex/huge repository or even thru several applications. It will be close to impossible to find every occurence reliably if the type is only a string. A wrapped primitive type gives you every occurence by the click of one hotkey, whatever your IDE is.
    – Loop
    Commented Mar 27, 2022 at 8:57
  • @Loop an even easier example is a GUID. Or any other ID. It's a string, however, you cannot enumerate all possible variations. Giving a meaningful label for what is expected is still useful.
    – VLAZ
    Commented Mar 27, 2022 at 16:33
16

OK, but doesn’t help as much as it could

This could be a helpful practice to communicate to other programmers what the expectation of the argument to a function is. It only helps programmers, though: TypeScript does not in any way enforce your requirement that some value be a FruitName rather than just any old string, because you’ve told TypeScript that FruitName and string are the exact same thing.

TypeScript is not confusing this for an object, or turning it into an object

Now, because of this, if I use alias in a function argument, and when I hover on it, my IDE shows that the type is FruitName. It gives a feeling that maybe FruitName is an object instead of a string.

TypeScript types are fully erasable—nothing you do in the “type domain” (any type alias definitions, anything included in an annotation after a : colon) will remain in the resulting JavaScript code. You can say anything you want in the types, and TypeScript will make sure they’re internally consistent but the actual JavaScript could do anything.

And TypeScript will understand that a FruitName could be used anywhere a string could be used, and be used any way a string could be used. Likewise, it will also understand that any string could be used any way a FruitName could be used—it won’t do any checking.

More powerful alternatives

There are better answers to this situation, depending on what you know up front and what your needs are.

Type literals—if you know ahead of time what strings are valid

A better approach—assuming you know ahead of time what valid fruits exist—is to use type literals. To limit redundant code, I like this approach for defining them:

const fruitNames = ['apple', 'banana', 'cantaloupe'] as const;
type FruitName = typeof fruitNames[number];

Note that fruitNames actually exists as a value in the runtime—it’s included in the JavaScript. That means you can run things like fruitNames.includes(someString) to check if the string is the name of a fruit.

The as const in the definition of fruitNames tells Typescript that fruitNames has the type readonly ['apple', 'banana', 'cantaloupe'], which means that FruitName is 'apple' | 'banana' | 'cantaloupe'. This is superior to writing out the definition of FruitName and then having to repeat yourself in the definition of fruitNames. If you don’t need a runtime list of fruits, just defining FruitName is fine.

Type branding—if you don’t know literal values, but want TypeScript to enforce things

So the issue with type FruitName = string; is that you can use any string anywhere a FruitName is requested; the FruitName type might assist programmers, but it doesn’t change anything about how TypeScript compiles anything. FruitName and string will be 100% interchangeable.

You can avoid that by using “type branding.” There’s a lot out there on this, but my preference is to use a utility type I call As:

declare abstract class As<Tag extends keyof never> {
  private static readonly $as$: unique symbol;
  private [As.$as$]: Record<Tag, true>;
}

(See this answer for the full details about why As is defined this way.)

With this, you can define FruitName like so:

type FruitName = string & As<'fruit-name'>;

Now since FruitName is a string, it can be used anywhere a string could be. However, FruitName is more than just a string: it is also—&—an As<'fruit-name'>. What does that mean? Absolutely nothing, as far as the compiled JavaScript is concerned. But to Typescript, it means that a FruitName is more than just a string—and that you can’t use any old string when a function requires a FruitName.

So we have:

type FruitName = string & As<'fruit-name'>;

declare const takesAnyString: (val: string) => void;
declare const takesOnlyFruitName: (val: FruitName) => void;

declare const someString: string;
declare const someFruit: FruitName;

takesAnyString(someString); // OK, obviously
takesAnyString(someFruit); // OK: FruitName is a string

takesOnlyFruitName(someFruit); // OK, obviously
takesOnlyFruitName(someString); // ERROR: someString is not a FruitName

This can be overkill in a lot of projects, in which case your plain alias could be a halfway step between just using string everywhere and defining type brands. But in a big project—particularly with a lot of special strings—it can be amazingly powerful for preventing bugs.

3
  • A much nicer to use opaque type for TS is described in this article - the difference is that you have implicit conversion when you do const fruit: FruitName = "apple" and you do not have explicit casting. Yet once you have the FruitName opaque type (called "flavoring" by the article) you cannot assign it to something else: const animal: AnimalName = fruit would throw a compile time error.
    – VLAZ
    Commented Mar 27, 2022 at 16:39
  • @VLAZ I mean, that is an extremely minor variation; all it requires is a ? added to the [As.$as$] property if that’s functionality you want. For me, I wouldn’t—I actually added my brandings to my code generator so it could use them, and I wanted other cases to be explicit about casting.
    – KRyan
    Commented Mar 27, 2022 at 18:15
  • 1
    This is a fantastic solution. Thank you! Commented Mar 28 at 2:59
4

A Type Alias Acts like a Named Constant for Types.

If you were looking at a max value for an input field, you may hardcode that value when checking it (e.g., value > 100) or you may store it in const maxValue = 100 and use it by name instead (e.g., value > maxValue). There are a few key benefits:

  1. it gives a name to the value for easier identification
  2. it lets you ignore the specific value in favor of how to use it
  3. it makes this definition in one, central, reusable place

These benefits apply to type aliases:

  1. A string could be anything, but a FruitName is obviously supposed to be the name of a fruit. You know not to use it directly as a file path or print it verbatim as an error message (unless of course the path or message is known to equal the fruit's name).
  2. Editor tools don't always tell you when FruitName is a string or an object - often, this is a good thing! Manipulating or interpreting a specialized, opaque(ish) type can then be done in a few, dedicated places that are meant to know the dirty details. Everything else simply passes the value around and uses these specialized options for anything more. This is especially valuable when you consider option 3.
  3. Suppose you need to do something fancy with FruitName. At the moment it's just "apple" or "pear", but you soon need to include sub-varieties like "Granny Smith" or "Concorde" as well. Everywhere you used string needs to be changed to the new type. If you aliased to FruitName and used that everywhere, that means you only need to change it in one place.

Obviously, the example given is simple and may not really benefit from aliasing. You don't need to give an alias to every logical set of values you have. But with more complicated situations, or especially those you haven't finalized the API for, type aliases for primitives and other simple types have benefits.

2
  • 2
    "You can't tell" - well, if it's a simple type alias, you can. It's not an opaque type, there's no newtype pattern here - and you can easily do parseInt(fruitName) or fruitName.toUpperCase() without the compiler complaining.
    – Bergi
    Commented Mar 25, 2022 at 23:19
  • @Bergi Actually, I was addressing the text in the question: "my IDE shows that the type is FruitName. It gives a feeling that maybe FruitName is an object instead of a string." I'll edit the question to clarify. Commented Mar 28, 2022 at 13:08
3

It is ok(since it clarifies the intent), but it is not great (since it is not enforced).

By lack of enforcement I mean that you can easily mix FruitName and PersonName if they are both strings.

You can make it much better by using type branding.

2

Type aliases are very helpful as part of refactoring, when you have to deal with a code base you're not familiar with. type TODO = unknown or type User = unknown are like comments that naturally go away when they're not needed anymore or evolve into an actual type. Your FruitName may just be a type alias now but over time it becomes type FruitName = 'Apple' | 'Banana' | string and when you drop the string part, it has become an actual type.

In many cases a plain type alias is not the correct choice because there's probably something else you should use instead. But it still carries slightly more information than just number, string, any or {} and when you can't think of anything else, use it.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.