- TypeScript 3.0: The Unknown Type
- Marius Schulz
- The Nuggets translation Project
- Permanent link to this article: github.com/xitu/gold-m…
- Translator: shixi – li
- Proofreader: Usey95, Smilemuffie
TypeScript 3.0 introduces the new unknown type, which is the safe counterpart of any.
The main difference between unknown and any is that the unknown type is more rigorous: before most operations can be performed on values of unknown type, we must perform some form of checking. We don’t have to do any checking before we operate on a value of type any.
This article focuses on the actual use of unknown types and includes comparisons to any types. If you need a more comprehensive code example to understand the semantics of unknown types, take a look at Anders Hejlsberg’s raw pull request.
any
type
Let’s first look at the any type so that we can better understand the motivation behind introducing unknown types.
The any type has been around since TypeScript’s first release in 2012. It represents all possible JavaScript values — primitive types, objects, arrays, functions, Error, Symbol, and any other values you might define.
In TypeScript, any type can be classified as any. This makes any the top-level type of the type system (also known as the global super type).
Here are some examples of code that we assign to any:
let value: any;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(a);// OK
value = Symbol("type"); // OK
Copy the code
The ANY type is essentially an escape pod for the type system. As developers, this gives us a lot of freedom: TypeScript allows us to do anything on values of type any without doing any kind of checking beforehand.
In the example above, the variable value is defined as type any. That’s why TypeScript considers all of the following operations to be type-correct:
let value: any;
value.foo.bar; // OK
value.trim(); // OK
value(); // OK
new value(); // OK
value[0] [1]; // OK
Copy the code
In many cases, this mechanism is too loose. Using the any type, it is easy to write code that is typed correctly but performs an exception. If we use any, we don’t enjoy TypeScript’s many protections.
But what if you could have top-level types and still be secure by default? That’s where Unknown comes in.
unknown
type
Just as all types can be classified as any, all types can also be classified as unknown. This makes Unknown another top-level type in the TypeScript type system (the other is any).
Here’s the same set of assignments we saw before, this time using a variable of type unknown:
let value: unknown;
value = true; // OK
value = 42; // OK
value = "Hello World"; // OK
value = []; // OK
value = {}; // OK
value = Math.random; // OK
value = null; // OK
value = undefined; // OK
value = new TypeError(a);// OK
value = Symbol("type"); // OK
Copy the code
All assignments to the value variable are considered typed correctly.
What happens when we try to assign a value of type Unknown to a variable of another type?
let value: unknown;
let value1: unknown = value; // OK
let value2: any = value; // OK
let value3: boolean = value; // Error
let value4: number = value; // Error
let value5: string = value; // Error
let value6: object = value; // Error
let value7: any[] = value; // Error
let value8: Function = value; // Error
Copy the code
An unknown type can only be assigned to any and the unknown type itself. Intuitively, this makes sense: only containers that can hold values of any type can hold values of unknown type. After all, we don’t know what type of value is stored in the variable value.
Now let’s see what happens when we try to operate on a value of type unknown. Here’s the same operation we’ve seen before:
let value: unknown;
value.foo.bar; // Error
value.trim(); // Error
value(); // Error
new value(); // Error
value[0] [1]; // Error
Copy the code
When the value variable type is set to unknown, these operations are no longer considered typed correctly. By changing the any type to unknown, we change our default setting from allowing everything to be flipped to almost nothing.
This is the main value proposition of the unknown type: TypeScript doesn’t allow us to do anything with a value of type unknown. Instead, we must first perform some type checking to narrow down the type range of the values we are using.
narrowunknown
Type range
We can narrow unknown types down to more specific types in different ways, including typeof operators, Instanceof operators, and custom type protection functions. All of these narrowing techniques contribute to TypeScript’s controlflow-based type analysis.
The following example shows how a value can get a more specific type in two if statement branches:
function stringifyForLogging(value: unknown) :string {
if (typeof value === "function") {
// Within this branch, `value` has type `Function`,
// so we can access the function's `name` property
const functionName = value.name| | "(anonymous)";
return `[function ${functionName}] `;
}
if (value instanceof Date) {
// Within this branch, `value` has type `Date`,
// so we can call the `toISOString` method
return value.toISOString();
}
return String(value);
}
Copy the code
In addition to using typeof or Instanceof operators, we can use custom type protection functions to narrow the scope of unknown types:
/** * A custom type guard function that determines whether * `value` is an array that only contains numbers. */
function isNumberArray(value: unknown) :value is number[] {
return (
Array.isArray(value) &&
value.every(element= > typeof element === "number")); }const unknownValue: unknown = [15.23.8.4.42.16];
if (isNumberArray(unknownValue)) {
// Within this branch, `unknownValue` has type `number[]`,
// so we can spread the numbers as arguments to `Math.max`
const max = Math.max(... unknownValue);console.log(max);
}
Copy the code
Although unknownValue has been classified as unknown, notice how it still gets the number[] type under the if branch.
rightunknown
Types use type assertions
In the previous section, we saw how typeof, Instanceof, and custom type protection functions can be used to convince the TypeScript compiler that a value has a certain type. This is a safe and recommended way to specify an “unknown” type as a more specific type.
If you want to force the compiler to trust a value of type Unknown to be of a given type, you can use a type assertion like this:
const value: unknown = "Hello World";
const someString: string = value as string;
const otherString = someString.toUpperCase(); // "HELLO WORLD"
Copy the code
Note that TypeScript doesn’t actually perform any special checks to ensure that type assertions are actually valid. The type checker assumes that you know better and believe that any type you use in a type assertion is correct.
If you make an error and specify the type of error, it is easy to throw an error at runtime:
const value: unknown = 42;
const someString: string = value as string;
const otherString = someString.toUpperCase(); // BOOM
Copy the code
The value variable value is a number, but we assume it is a string and use type assertion value as string. So be careful with type assertions!
In the union typeunknown
type
Now let’s take a look at how unknown types are handled in union types. In the next section, we’ll also look at crossover types.
In a union type, an unknown type absorbs any type. This means that if either component type is unknown, the union type is also equivalent to unknown:
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
Copy the code
One surprise to this rule is the any type. If at least one of the composition types is any, the union type is equivalent to any:
type UnionType5 = unknown | any; // any
Copy the code
So why can unknown absorb any type (except any)? Let’s think about unknown | string this example. This type can represent any unkown or string value. As we learned before, all types of values can be defined as unknown types, including all of the string type, therefore, unknown | string is said and unknown type itself the same set of values. Therefore, the compiler can reduce the union type to the unknown type.
Cross typeunknown
type
In a crossover type, any type can absorb an unknown type. This means that intersecting any type with Unknown does not change the result type:
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
Copy the code
Let’s review IntersectionType3: Unknown & String represents all values that can be assigned to both unknown and String. Since each type can be assigned to unknown, including unknown in a crossover type does not change the result. We’re just going to be left with string.
The use type isunknown
Operator of the value of
A value of type unknown cannot be used as an operand of most operators. This is because most operators are unlikely to yield meaningful results if we do not know the type of value we are using.
There are only four equality and inequality operators that you can use on values of type unknown:
= = =
= =
! = =
! =
If you want to use any other operator on a value of type unknown, you must first specify the type (or use a type assertion to force the compiler to trust you).
Example: from thelocalStorage
Reads the JSON
This is a real example of how we use unknown types.
Suppose we want to write a function that reads the value from localStorage and deserializes it into JSON. If the item does not exist or is invalid JSON, the function should return an error result; otherwise, it should deserialize and return the value.
Because we don’t know what type of value we’ll get after deserializing the persistent JSON string. We will use Unknown as the deserialized value type. This means that the caller of our function must perform some form of checking (or use type assertion) before performing an operation on the return value.
Here’s how we implement this function:
type Result =
| { success: true, value: unknown }
| { success: false, error: Error };
function tryDeserializeLocalStorageItem(key: string) :Result {
const item = localStorage.getItem(key);
if (item === null) {
// The item does not exist, thus return an error result
return {
success: false,
error: new Error(`Item with key "${key}" does not exist`)}; }let value: unknown;
try {
value = JSON.parse(item);
} catch (error) {
// The item is not valid JSON, thus return an error result
return {
success: false,
error
};
}
// Everything's fine, thus return a success result
return {
success: true,
value
};
}
Copy the code
The return value type Result is a tagged union type. In other languages, it can also be called Maybe, Option, or Optional. We use Result to clearly simulate the successful and unsuccessful results of an operation.
TryDeserializeLocalStorageItem the function of the caller before trying to use the value or the error attribute must first check success attributes:
const result = tryDeserializeLocalStorageItem("dark_mode");
if (result.success) {
// We've narrowed the `success` property to `true`,
// so we can access the `value` property
const darkModeEnabled: unknown = result.value;
if (typeof darkModeEnabled === "boolean") {
// We've narrowed the `unknown` type to `boolean`,
// so we can safely use `darkModeEnabled` as a boolean
console.log("Dark mode enabled: "+ darkModeEnabled); }}else {
// We've narrowed the `success` property to `false`,
// so we can access the `error` property
console.error(result.error);
}
Copy the code
Note that tryDeserializeLocalStorageItem function cannot be simply represented by returning null deserialization failure, reasons are as follows:
null
Value is a valid JSON value. Therefore, we cannot distinguish between the pair valuesnull
Deserialization was performed, and again the entire operation failed due to missing arguments or syntax errors.- If we return from a function
null
We cannot return an error at the same time. Therefore, the caller of our function does not know why the operation failed.
For completeness, a more mature alternative to this approach is secure JSON parsing using a type decoder. The decoder requires us to specify the expected data structure of the value to be deserialized. If the persisted JSON result does not match the data structure, decoding will fail in a well-defined way. Thus, our functions always return valid or failed decoding results, and the unknown type is no longer needed.
If you find any mistakes in your translation or other areas that need to be improved, you are welcome to the Nuggets Translation Program to revise and PR your translation, and you can also get the corresponding reward points. The permanent link to this article at the beginning of this article is the MarkDown link to this article on GitHub.
The Nuggets Translation Project is a community that translates quality Internet technical articles from English sharing articles on nuggets. The content covers Android, iOS, front-end, back-end, blockchain, products, design, artificial intelligence and other fields. If you want to see more high-quality translation, please continue to pay attention to the Translation plan of Digging Gold, the official Weibo, Zhihu column.