This content originally appeared on DEV Community and was authored by Jesse Warden
There are a few options, and strategies. We’ve listed from easiest to most thorough.
- Use any, then ask for help.
- Use unknown
- Use a Record with a string key and whatever value, like
Record<string, any>
. - Use
IDontKnowYet
as an aliased type or interface. - For a multitude of types, use an anonymous Union, like
string | number
. If you still don’t know, usestring | number | any
since we’ll knowany
is the outlier. - Use a Pick or Partial of an existing type you may already have nearby.
Option 1: any
You may not be comfortable, or know the ramifications, of using another type. That’s fine, no worries! Just use any
, then ask the community for help (Discord, Slack, forums, dev.to, Reddit, etc). You’ll find that there are certain types of devs that love to brainstorm… about types. Given TypeScript is a gradual type system, you have a lot more options in degrees in complexity so discussing your options and the tradeoffs can help. The key is to “keep TypeScript compiling successfully” and then slowly add changes to remove the any. The temptation can be to use more complicated types, TypeScript stops compiling, and may give you compilation errors that aren’t always clear. This may discourage you from removing the any
.
Even if the any
is only being used in 1 place in the code, you should still give it a name.
type IDontKnow = any
We can then change the name once we learn about what it is used for:
type APIResponse = any</code></span>
Unknown Return
To avoid using void
or “I don’t know, but TypeScript I think does”.
function stuff():any {
// TODO: I don't know what this actually returns, plz halp...
}
Even better, give it a name:
type StuffReturnValue = any
function stuff():StuffReturnValue {
// TODO: I don't know what this actually returns, plz halp...
}
Unknown Parameter
If you’re unsure what is sent into a method / function:
// I don't know what these params are, or rather, should be.
getContract(params:any):Observable<ApplicationContract> {
...
}
To differentiate it, give the parameters type a name:
type GetContractParameters = any
getContract(params:GetContractParameters):Observable<ApplicationContract> {
...
}
Option 2: unknown
The nice thing about unknown
is that you can’t assign it to anything without safely converting it first. The bad thing is you have to use type narrowing. If you’re just learning JavaScript and TypeScript, this can be a lot of work to help the system.
Examples include using if statements with typeof
, or instanceof
, or type constructors like String(thing)
or parseInt(maybeANumber)
. There are a lot of type narrowing options you can do to help TypeScript help you ensure you’re converting the unknown
type to the correct type. Using any
, while you have some narrowing options, it can still be unsafe to convert it. The unknown
type at least forces you to try which increases the likelihood the code will be safer and possibly more correct.
someMethod(input:unknown):void {
// TODO: cast input to number,
// or string,
// and if those don't work,
// then just log an error
// with the input so we can
// attempt to figure out its type
// is it a real number?
if(typeof input === 'number' && isNaN(parseInt(input)) === false) {
...
// is it a non empty, not blank, string?
} else if(typeof input === 'string' && input !== '' || input !== ' ') {
...
} else {
console.error(`Failed to convert input, it's not a string or a number: ${input}`)
}
}
Because it truly is unknown, you can name it with the possibilities:
type StringOrNumberOrNotSure = unknown
someMethod(input:StringOrNumberOrNotSure):void {
...
}
Option 3: Record
Record’s are typically used to combine a few types together into a type that allows you to use custom property and value types. However, even same Record<string, string>
which is basically the same as { name: 'Jesse'}
at least says A LOT about the type, such as:
- It says it’s an Object, and not a primitive like string, number, etc
- It says it’s always a value, instead of sometimes
undefined
ornull
That already is insanely clear. Even saying the value is any
like Record<string, any>
is still wonderful because we know it’s some type of normal JavaScript object that has string names for values we’re unclear of right now. Wonderful first step.
Caveat: Record’s are safer with generic type values. Specific types, if they don’t match, TypeScript will incorrectly assume properties exist, with a type, that do not.
const bad: Record<string, number> = { foo: 9 };
// At runtime this is `undefined` but at type-check time typescript thinks it is a `number`
const incorrect: number = bad["does not exist"];
type StringObject<V> = {[key: string]: V | undefined};
const good: StringObject<number> = { foo: 9 };
// Type error 'number | undefined' is not assignable to type 'number'.
const typeError: number = good["does not exist"];
const correct: number | undefined = good["does not exist"];
Option 4: Aliased Type Object
Create a type or interface, give it a name, and don’t give it any properties. This gives manifest the scaffolding of your idea of what the any
might be in the future.
type DudeIDontKnow = {}
Awesome! If you know of a property it may have, add it:
type DudeIDontKnow = {
name:string
}
However, if you’re unsure, just make it optional:
type DudeIDontKnow = {
name?:string
}
It may not yet be clear what the type should be, but you’ve at least given it a name, and knowing the name gives you power over the thing.
Caveat: You’ll often see interface
and other times see type
. The interface
typically implies it’s some time of class instance, which may/may not have inheritance, whereas a type
implies a simple JavaScript Object. While you can use intersection types with type
, it’s rare, at least in the Functional Programming mindset. Use whichever one you like, interface
or type
.
Option 5: Union
Sometimes the type can be 1 of many types. For that we use a Union. For example, if you know it’s usually a string, but sometimes not, you can use string | undefined
. That means it’s either a string, or undefined, and those using the type will have to handle both. TypeScript has gotten really good about helping avoid null pointer exceptions at runtime (aka “undefined is not a function”), and openly acknowleding something may be undefined sometimes is a great first step.
Another situation is when you’re dealing with a back-end system that may send a few different types and you’re still figuring them out. Let’s say you know there might be 3, and you know 2 of them, you can define the first 2, but then leave the 3rd as any to give yourself an out. Like GoodResponse | MoreDataNeeded | any
. This means you have a nice type for GoodResponse
defined, another nice one for MoreDataNeeded
, but you know the API returns other things you’re not sure of. Using any
in this context will at least make it clear.
To make it even safer, use GoodResponse | MoreDataNeeded | unknown
to ensure the unknown path is handled in code more clearly and safely, even if you’re not really sure what you’re checking for. This is a great second step.
Finally, whatever union you come up with, give it a name vs. keeping it anonymous. Treat it just like you’d treat a named type/interface, like:
type APIResponse = GoodResponse | MoreDataNeeded | unknown
Option 6: Partial, Pick, and other Utility Types
There are a variety of utility types TypeScript has, and Partial is one where you can create a type from a piece of another one that may require too much typing because it has a lot of properties. Partial will make them all optional. If you already have a type, say a large block of JSON from an API, Partials’ can help make it saner to work with. If you only need 4 fields from the 32 field JSON type, just create a Partial that includes the 4 you need. While the Partial will include all the other 32 fields, it’ll set them all to optional.
If you really want just the 4 fields, the Pick type can help you snag just the ones you want.
The only thing to be careful of is that Partial
and Pick
can stick around longer than they should. They should be treated like Feature Flags; deleted after they’ve been used. Think of them like leftovers in the fridge. A few days is fine, but after 4, dude, throw it out.
Why Care
Using types removes a whole set of bugs. The more types you use can decrease the likelihood of bugs at runtime. A little bit of type effort pays off in bug prevention and runtime exceptions. Many tests do not need to be written when using strong types.
Caveat: Gradually Typed
However, TypeScript is gradually typed. Unlike strong and soundly typed languages, you can choose how strict your types are. This has pros and cons.
Pro’s
The first pro is you can run raw JavaScript through TypeScript and without changing anything it can find potential issues with it’s type inference; guessing, often correctly, about possible issues in your code. Even adding just a few types can help guide TypeScript in the right direction.
The second is you can integrate with code that isn’t yours. If the library developer, or existing code base doesn’t provide types, you can provide the types for TypeScript either through type definition files or through ambient declarations so your code using those dependencies are safer. And example is the document
object in a web browser. You may accidentally misspell getElementByID
in your code base (the ending D
is supposed to be lowercase). While these are provided by JavaScript, this helps in other places JavaScript is utilized such as Node.js, device environments such as Raspberry PI, etc.
Third, the types can scale in how narrow and strict you wish to make them. This means if you’re learning TypeScript, you can make the types stricter as you get better at it. If you’re still learning your problem domain, you can increase the strictness of the types as you work and explore. If you’re integrating with code and you have no idea what the types are because the JavaScript is a black box and has no documentation, you can add types as you learn more how it works at runtime.
Fourth, you can use the above at whatever pace you want. This means you can start with just unit tests, or just one file, or 1 module/class. You can either use it throughout your codebase, but then start to increase how strict the compiler is via the compiler settings. This helps teams who aren’t familiar with TypeScript, but are familiar with JavaScript, to slowly adopt it with less risk, and still be productive.
Cons
The con is, the less strict the types, the more likely TypeScript, or you, are wrong about what is actually happening at runtime. This leads to bugs that should have been prevented with types, but the types were either wrong, or not thorough enough.
TypeScript is also more verbose compared to other non-gradually typed languages, much like Python’s Typings or Roblox’ typed Lua called Luau. This is because it needs to be flexible about all the possible scenarios that can, and do occur in dynamic typed languages that do not follow strongly typed rules. This means they need to be way more flexible, and handle more scenarios. While a pro in terms of comprehensiveness, it has performance tradeoffs, and more importantly for programmers, readability tradeoffs. Some developers will intentionally not add stricter types either because they are too verbose to understand, or are perceived as a lot of work with not enough return on value. To be clear, though, it’s not nuanced at the extremes.
Why Any Can be Good
The any
type is actually a valid type, meaning the value truly can be anything. The default for TypeScript when it cannot infer a type and the developer has written anything is any
. However, intentionally saying any means it can truly be anything. When you get certain callbacks or functions that can take more than one type, this can be a valid addition. Many 3rd party libraries built in JavaScript either did not care much about the type possibilities, viewing that as a feature of dynamic languages, or simply did not think through all the different code paths providing a variety of types would cause on the developer forced to use their code.
Why Many View any as Bad
The main reasons are having to handle too many code paths, more unit tests than are needed which we also have to maintain, unexpected bugs, runtime exceptions in production, and lack of understanding of the code.
Narrowing The Problem
The whole point of using types is to narrow your problem. That means shrinking how many situations you need to handle. If a function returns a string | undefined
, that means you need to handle both code paths for when it returns a string, but also handle when it returns an undefined value. If it just returned string
, it’d be a lot less code.
Another example, most of the Math
functions in JavaScript look like they take numbers only, like Math.abs
and Math.round
. However, they take many other types, such as String
, and attempt to convert to a Number for you. They even take Date
s! This is a lot of code paths for the JavaScript engine to handle, a lot of different optimizations they have to handle, and a lot of backwards compatibility concerns as well.
Less Code, Less Tests
Having to handle that in code is not something we want to sign up for. Less types, less situations, and solving just the business problem at hand is the goal. Using types to narrow what you have to deal with has a lot of great things, but the 2 we most care about:
- Making impossible situations impossible.
- Reducing how many unit tests we need to write.
Using any
can allow situations we didn’t know we need to handle in the code happen at runtime, unexpectedly, in Production. That’s not good. Better to find that earlier before we ship code to our users. Certain situations in code we didn’t think could happen, do happen, because any tells the compiler it could happen. Worse, we sometimes have to write code because something is any
, and have to handle all possible type situations (e.g. we need a string
, but if it’s undefined
, handle that code path as well, even if it doesn’t make sense for it to be undefined
).
Additional Unit Tests
Having to write unit tests to compensate for the lack of typing is a must. It ensures when you change code, you know if you didn’t break anything. With any
, there is very little compiler support, so we have to write unit tests instead in case an unhappy path happens, and we confirm we’ve handled it. The issue there is the unit test makes an assumption around the type, or types, the unhappy path should handle. Because it’s literally anything, we could have missed one or more so unit tests aren’t a sure fire way.
Lack of Understanding
The point of “typing things”, specifically around Objects and classes is to identify “what they are”. Examples would be:
- “That’s a Google Maps Address”
- “That is a Configuration, how the UI component is configured from our Content Management System”
- “That is a list of Products, credit card offers with different benefits and brands that are user may qualify for”
Those simple types convey an immense amount of information, and are a shared language we all use to effectively communicate. We all, Product, Design, Developers, can use the same word for Product
, and have a shared understanding.
Using any
, we don’t. No one knows what it is, there is no shared understanding; there’s actually no understanding. Other team members, or yourself 3 months later, has no idea what the type is supposed to be, or even ought to be.
This content originally appeared on DEV Community and was authored by Jesse Warden
Jesse Warden | Sciencx (2024-09-07T12:49:11+00:00) Six Alternatives to Using any in TypeScript. Retrieved from https://www.scien.cx/2024/09/07/six-alternatives-to-using-any-in-typescript/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.