Before the words
“TypeScript” gives “JavaScript” the ability to define types, allowing JS to retain the experience of dynamically typed programming languages while enjoying the rigorous type checking at compile time of statically typed programming languages.
When you are new to TypeScript, you are familiar with basic type definitions, interface declarations, inherited polymorphism, and generics. When you define simple object types or function input and output in a project, you simply use “any” when you encounter complex types. In extreme cases, TypeScript projects become “AnyScript”.
This clearly defeats TypeScript’s purpose and fails to fully use the capabilities TypeScript provides.
Keep in mind that if you decide to use TypeScript as your main development language, try to type variables and so on correctly.
This article is only my personal understanding after studying the official documents. If there are any mistakes, please point out and correct them.
In fact, TypeScript has a number of “postures” that help us write complete, type-correct code, but we may not know about them. This article will briefly introduce some of the postures that can be used in real projects.
Let’s begin the text.
The body of the
1. Use any or unknown?
When TypeScript is first released, it provides type “any”. A mutable variable can accept a value of “any” if it is declared to be of type any, as follows:
let val: any;
val = 1; // Accepts a variable of type "number"
val = 'a'; // Accepts a variable of type string
val = () = > console.log('hello world'); // Accept a variable of type "Function"
Copy the code
But again, because it is “any value”, it does anything that TS thinks is correct at compile time, and therefore is prone to runtime exceptions, as follows:
let val: any;
val = 1;
val.hello(); // No errors are reported at compile time, but exceptions are thrown at run time
Copy the code
So if you want to accept any value with a variable, but don’t want to be “100% trusted” and write a bug, it’s better to use the “unknown” type.
The biggest difference between the “unknown” type and the “any” type is that you have to “determine its type” or “assert its type” before you can do anything with an “unknown” variable.
function fn(val: unknown) {
val.toLowerCase(); // ts type check error, compilation exception
val(); // ts type check error, compilation exception
}
Copy the code
1.1 Determine its type
“Determine its type” is easy to understand, and the code is shown as follows:
function fn(val: unknown) {
if (typeof val === 'string') { // The value is a string
console.log(val.toLowerCase());
} else if (typeof val === 'function') {judge as"function"Typeval(); }}Copy the code
1.2 Assert its type
An “assertion” is a cast of “unknown” to a known type in code logic.
This makes sense, because there are cases where we are certain of its type, as follows:
let val: unknown;
val = 'hello world';
const str = val as string; // Assert val as type "string"
console.log(str);
Copy the code
After “assert to variable”, the TS compiler will “believe 100%” and therefore pass the check directly at compile time. Therefore, assertion is an error-causing write bug action (compile pass, runtime exception) that needs to be taken care of during assertion.
1.3 summary
By now the reader should have a rough idea of the difference between “any” and “unknow,” so if you want to use a “universal” type in a project, lean toward “unknown” as much as possible.
Unknown is a narrower type than any and takes advantage of TypeScript’s type-qualifying capabilities.
This is not to ban the “any” type. “any” is also an important type that can be used in situations where it is irreplaceable, such as when newcomers use it in the first place — “The type is too complex to define, but I know it can be used for XXX.”
2. Void and never?
I’m going to talk about both of them because they’re similar.
2.1 as the variable type
Void can accept undefined, but never can accept never.
let a: void;
a = undefined; // Compile properly
let b: never;
b = undefined; // Type check error
let c: never;
c = b; // Compile properly
Copy the code
In the correct use scenario, variables of type “never” do not exist, as demonstrated by the “enum” type code:
enum EType {
"a" = "a"."b" = "b",}function fn(type: EType) {
switch (type) {
case EType.a:
break;
case EType.b:
break;
default:
// the val variable is of type "never"
// Since case already handles all enumerated scenarios, executing code to default should be an exception scenario
const val = type;
break; }}Copy the code
2.2 as the return value type
A function that returns “void” can actually return any value, but the variable that accepts the return value is of type “void”. This is reasonable because a variable that accepts a return value cannot “do anything”, as follows:
type IFn = () = > void;
const fn1: IFn = () = > {
return; // Compile properly
};
const t1 = fn1(); // The value type is "void"
const fn2: IFn = () = > {
return "Hello"; // Compile properly
};
const t2 = fn2(); // The value type is "void"
console.log(t2.toLowerCase()); // Type check error
Copy the code
The return value of “never” only exists in the “infinite loop” or “exception” scenario, the code explains as follows:
type IFn = () = > never;
/** infinite loop function **/
const fn1: IFn = () = > {
while (true) {
// Compile properly}};/** throws the exception function **/
const fn2: IFn = () = > {
throw new Error(a);// Compile properly
};
Copy the code
The above code nicely explains that variables of type “never” don’t exist.
3. How do I get the subattribute type of a type?
First of all, what does this subtitle mean? There is a scenario in real development that uses an open source component library that exports a complex data type as follows:
export interface IData = {
id: string;
user: {
name: string;
info : {
avatar: string; }}}Copy the code
If we need to pre-define a variable to receive the “user” attribute in this complex type, how should the “user” variable type be defined?
There is a low way to do this, using “typeof” as follows:
import { data } from 'xxx';
let val: typeof data.user; // use typeof to get the typeof data.user. val = data.user;// Receive the user value of the data subattribute
Copy the code
The most standard way to do this is to define a type as if it were an object property.
import { data, IData } from 'xxx';
let val: IData['user']; // Define the type as if it were an object property. val = data.user;// Receive the user value of the data subattribute
Copy the code
What’s the use of in keyof?
“In keyof” is commonly used, but the most common is “use it to rewrap a type.” For example, the following form type exists:
type IForm = {
name: string;
age: number;
avatar: string;
}
Copy the code
I’m going to wrap all the subattribute types around a layer of “value”, such as name: {value: string; }, then you can wrap it with “in keyof” :
type IFormValue = {
[K in keyof IForm]: {
value: IForm[K]; }}Copy the code
With “Get subattribute type” we can also restore “IFormValue” to “IForm” as follows:
type IForm = {
[K in keyof IFormValue]: IFormValue[K]['value'];
}
Copy the code
“In keyof” can do more than that
5. Use extends to qualify generic types
The power of “generics” is undeniable, allowing the following “functions” to be reused and return values of the “exact type” :
function fn<T> (val: T) {
console.log(`Hello ${val}`);
return val;
}
fn(1996); // 'Hello 1996'
fn("Pwcong"); // 'Hello Pwcong'
fn({
toString: function () {
return "ByteDance"; }});// 'Hello ByteDance'
Copy the code
We can pass in “number”, “string” or even some “object” type that overrides the “toString” implementation, but once we pass in an “unexpected” type, the function prints weird logs.
A better way for functions that use “generics” is to use “extends” to qualify the type passed in, as follows:
function fn<T extends number | string> (val: T) {
console.log(`Hello ${val}`);
return val;
}
fn(1996); // The compiler passes
fn("pwcong"); // The compiler passes
fn({
toString: function () {
return "ByteDance"; }});// Type check error
Copy the code
6. Common tool types
⚠ ️ note: article here began to demonstrate simple common “tool type”, the complete “type tool” document please refer to the official document address: www.typescriptlang.org/docs/handbo…
6.1 Partial: Optional sub-attribute
// All attributes of "IForm" type "Required"
type IForm = {
name: string;
age: number;
avatar: string;
}
typeINewForm = { name? :string; age? :number; avatar? :string;
}
// This type is the same as the previous type, use Partial to change all of its properties to optional.
type INewForm = Partial<IForm>;
Copy the code
6.2 Record: User-defined object type
type IForm = { [key: string] :string | number }
// This type is the same as the above type, using the "Record" custom object type
type INewForm = Record<string.string | number>;
const form: INewForm = {
name: 'Pwcong'.age: 18.avatar: 'https://xxx'};// The compiler passes
Copy the code
6.3 Pick: Selects subattributes of the type
type IForm = {
name: string;
age: number;
avatar: string;
}
typeINewForm = { name? :string; avatar? :string;
}
// This type is the same as the above type, use "Pick" to Pick the type subattributes
type INewForm = Pick<IForm, 'name' | 'avatar'>;
Copy the code
6.4 Omit: Exclude type subattribute
type IForm = {
name: string;
age: number;
avatar: string;
}
typeINewForm = { name? :string;
}
Use "Omit" to Omit type subattributes
type INewForm = Omit<IForm, 'age' | 'avatar'>;
Copy the code
6.5 Exclude: Excludes the association type
type IType = number | string | boolean
type INewType = number
Use "Omit" to Omit type subattributes
type INewType = Exclude<IType, string | boolean>
Copy the code
6.6 Parameters: Obtains the type of function Parameters
type IFn = (name: string, age: number) = > boolean;
type IName = Parameters<IFn>[0]; // String
type IAge = Parameters<IFn>[1];// "number" type
Copy the code
6.7 ReturnType: The type of the return value of the function
type IFn = () = > string;
type IRet = ReturnType<IFn>; // String
Copy the code
conclusion
We’ve covered some “common TypeScript postures” here. I hope you’ll find them useful and correct any errors you make