hi, bean skin powder, meet you again today. This issue shares the advanced use of TypeScript brought by bytedancer "little blacksmith in Milan",
It is applicable to students who have known TypeScript or have actually used it for a period of time,
This article systematically introduces the function points of common TypeScript from the perspective of type, operator, operator and generic,
Finally, share the author's practical experience.
Author: little blacksmith in Milan
1, Type
unknown
unknown refers to a type that cannot be predefined. In many scenarios, it can replace the function of any while retaining the ability of static inspection.
const num: number = 10; (num as unknown as string).split(''); // Note that here, like any, it can pass the static check completely
At this time, the function of unknown is highly similar to that of any. You can convert it into any type. The difference is that during static compilation, unknown cannot call any method, while any can.
const foo: unknown = 'string'; foo.substr(1); // Error: an error is reported if the static check fails const bar: any = 10; bar.substr(1); // Pass: any type is equivalent to abandoning static checking
One usage scenario of unknown is to avoid static type checking bug s caused by using any as the parameter type of the function:
function test(input: unknown): number { if (Array.isArray(input)) { return input.length; // Pass: in this code block, the type guard has identified input as array type } return input.length; // Error: the input here is of unknown type, and an error is reported in static check. If the input parameter is any, it will give up the inspection and directly succeed, resulting in the risk of error reporting }
void
In TS, void and undefined functions are highly similar, which can logically avoid errors caused by careless use of null pointers
function foo() {} // This empty function does not return any value. The default return type is void const a = foo(); // At this time, the type of a is defined as void, and you cannot call any attribute method of A
The biggest difference between void and undefined types is that you can understand that undefined is a subset of void. When you don't care about the return value of the function, use void instead of undefined. Give a practical example in React.
// Parent.tsx function Parent(): JSX.Element { const getValue = (): number => { return 2 }; /* Here, the function returns the type number */ // const getValue = (): string => { return 'str' }; /* Here, the string type returned by the function can also be passed to the child attribute*/ return <Child getValue={getValue} /> }
// Child.tsx type Props = { getValue: () => void; // void here means that you don't pay attention to the specific return value type logically. number, string, undefined, etc. are OK } const Child = ({ getValue }: Props) => <div onClick={() => getValue()}>click</div>;
never
never refers to the type that cannot be returned normally. A function that must report an error or an endless loop will return such a type.
function foo(): never { throw new Error('error message') } // The return value of throw error is never function foo(): never { while(true){} } // This loop will not exit normally function foo(): never { let count = 1; while(count){ count ++; } } // Error: the return value cannot be defined as never because it cannot be recognized directly in the static compilation phase
There are also types that never intersect
type human = 'boy' & 'girl' // These two separate string types cannot intersect, so human is of type never
However, any type combined with the never type is still the original type
type language = 'ts' | never // language type or 'ts' type
never has the following features:
- After calling a function that returns never in a function, the following code becomes deadcode.
function test() { foo(); // foo here refers to the function that returns never above console.log(111); // Error: the compiler reports an error. This line of code will never be executed }
- Cannot assign another type to never
let n: never; const o: any = {}; n = o; // Error: you cannot assign a non never type to a never type, including any
There are some hack usages and discussions about the feature of never, such as this one You Yuxi's answer
2, Operator
Non empty assertion operator!
This operator can be used after the variable name or function name to emphasize that the corresponding element is non null and undefined
function onClick(callback?: () => void) { callback!(); // The parameter is optional, and this exclamation point is added! After that, TS compilation does not report errors }
After checking the compiled ES5 code, I didn't make any judgment
function onClick(callback) { callback(); }
This symbol is especially suitable for scenarios where we already know that null values will not be returned, so as to reduce redundant code judgment, such as React's Ref
function Demo(): JSX.Elememt { const divRef = useRef<HTMLDivElement>(); useEffect(() => { divRef.current!.scrollIntoView(); // useEffect is triggered only after the component is mounted, so current must have value }, []); return <div ref={divRef}>Demo</div> }
Optional chain operator
Compared to the above! Non null judgment used in compilation stage This is the non empty judgment of the runtime (which is also valid at compile time) that developers need most
obj?.prop obj?.[index] func?.(args)
?. It is used to judge whether the expression on the left is null | undefined. If so, the expression will stop running, which can reduce a lot of & & operations
For example, let's write a B, the compiler will automatically generate the following code
a === null || a === void 0 ? void 0 : a.b;
Here is a small knowledge point: the value of undefined will be re assigned in the non strict mode. Using void 0 must return the real undefined value
Null value merge operator??
?? Its function is similar to that of 𞓜 except that 𞓜?? The right expression is returned only when the result of the left expression is null or undefined
For example, we wrote const B = a?? 10. The generated code is as follows
const b = a !== null && a !== void 0 ? a : 10;
The | expression, as we all know, will also take effect for logical null values such as false, '', NaN and 0. It is not suitable for us to merge parameters
Number separator_
const num:number = 1_2_345.6_78_9
_ It can be used to separate long numbers arbitrarily. It is mainly designed to facilitate the reading of numbers. The compiled code is not underlined. Please rest assured
3, Operator
Get keyof from key value
keyof can obtain all key values of a type and return a union type, as follows
type Person = { name: string; age: number; } type PersonKey = keyof Person; // The type obtained by PersonKey is' name '|' age '
A typical use of keyof is to restrict the legalization of the key of the access object, because any indexing is not accepted
function getValue (p: Person, k: keyof Person) { return p[k]; // If K is not so defined, it cannot be compiled in the code format of p[k] }
To sum up, the syntax format of keyof is as follows
type = keyof type
Get typeof instance type
typeof is to get the type of an object / instance, as follows
const me: Person = { name: 'gzx', age: 16 }; type P = typeof me; // { name: string, age: number | undefined } const you: typeof me = { name: 'mabaoguo', age: 69 } // Can be compiled
Typeof can only be used on specific objects, which is consistent with typeof in js, and it will automatically determine which behavior should be performed according to the left value
const typestr = typeof me; // The value of typestr is "object"
Typeof can be used with keyof (because typeof returns a type), as follows
type PersonKey = keyof typeof me; // 'name' | 'age'
To sum up, the syntax format of typeof is as follows
type = typeof Instance object
Traversal attribute in
In can only be used in the definition of types. You can traverse enumeration types as follows
// This type can convert any type of key value into number type type TypeToNumber<T> = { [key in keyof T]: number }
keyof returns all key enumeration types of generic T. key is any custom variable name, which is linked with in in the middle and wrapped with [] (this is a fixed collocation). Number on the right of colon defines all keys as number type.
So it can be used like this
const obj: TypeToNumber<Person> = { name: 10, age: 10 }
To sum up, the syntax of in is as follows
[ Custom variable name in Enumeration type ]: type
4, Generics
Genericity is a very important attribute in TS, which carries the bridge from static definition to dynamic call. At the same time, it is also the meta programming of TS's own type definition. Generics can be said to be the essence of TS type tools and the most difficult part of the whole ts. here is a summary in two chapters.
Basic use
Generics can be used in common type definitions, class definitions and function definitions, as follows
// Common type definition type Dog<T> = { name: string, type: T } // Common type use const dog: Dog<number> = { name: 'ww', type: 20 } // Class definition class Cat<T> { private type: T; constructor(type: T) { this.type = type; } } // Class use const cat: Cat<number> = new Cat<number>(20); // Or const cat = new Cat(20) // Function definition function swipe<T, U>(value: [T, U]): [U, T] { return [value[1], value[0]]; } // Function use swipe<Cat<number>, Dog<number>>([cat, dog]) // Or short for swipe([cat, dog])
Note that if a generic type is defined for a type name, the generic type must also be written when using this type name.
For variables, if their type can be inferred at the time of call, generic writing can be omitted.
The syntax format of generics is briefly summarized as follows
Type name<Generic list> Specific type definition
Generic derivation and default values
As mentioned above, we can simplify the writing of generic type definition, because TS will automatically deduce the variable type according to the type of variable definition, which usually occurs in the case of function call
type Dog<T> = { name: string, type: T } function adopt<T>(dog: Dog<T>) { return dog }; const dog = { name: 'ww', type: 'hsq' }; // Here, an object of type string is defined according to the Dog type adopt(dog); // Pass: the function will infer that the type is string according to the input parameter type
If function generic derivation is not applicable, we must specify generic type if we need to define variable type
const dog: Dog<string> = { name: 'ww', type: 'hsq' } // The < string > section cannot be omitted
If we don't want to specify, we can use the scheme of generic default value
type Dog<T = any> = { name: string, type: T } const dog: Dog = { name: 'ww', type: 'hsq' } dog.type = 123; // However, in this way, the type is any, which cannot be deduced automatically and loses the meaning of generics
The syntax format of generic default values is briefly summarized below
Generic name = Default type
Generic constraints
Sometimes, we can not focus on generic and specific types, such as
function fill<T>(length: number, value: T): T[] { return new Array(length).fill(value); }
This function accepts a length parameter and a default value, and the result is to generate an array with the corresponding number filled with the default value. We don't need to judge the parameters passed in, just fill them in directly, but sometimes we need to limit the type. At this time, we can use the extends keyword
function sum<T extends number>(value: T[]): number { let count = 0; value.forEach(v => {count += v}); return count; }
In this way, you can call the summation function in the way of sum([1,2,3]), which cannot be compiled
Generic constraints can also be used in the case of multiple generic parameters
function pick<T, U extends keyof T>(){};
This means that U must be a subset of T's key type. This usage often appears in some generic tool libraries.
The syntax format of extensions is briefly summarized as follows. Note that the following types can be either general types or generic types
Generic name extends type
Generic condition
The extensions mentioned above can also be used as a ternary operator, as follows
T extends U? X: Y
Here, there is no restriction that T must be a subtype of U. if it is a subtype of U, T is defined as type X, otherwise it is defined as type Y.
Note that the generated results are distributive.
For example, if we replace X with T, the form is: T extends U? T: never
At this time, the returned t is the part that satisfies the original t including u, which can be understood as the intersection of T and U
Therefore, the syntax format of extends can be extended to
Generic name A extends type B ? type C: type D
Generic inference infer
infer in Chinese means "inference", which is generally used with the above generic conditional statements. The so-called inference is that you don't need to specify it in the generic list in advance, and it will be judged automatically at run time, but you have to predefine the overall structure first. for instance
type Foo<T> = T extends {t: infer Test} ? Test: string
First look at the content behind extensions, {t: infer Test} can be regarded as a type definition containing the t attribute. The value type of the t attribute will be assigned to the Test type after inferring through infer. If the actual parameters of the generic type meet the definition of {t: infer Test}, the Test type will be returned, otherwise it will be the default string type.
Take an example to deepen understanding
type One = Foo<number> // string, because number is not an object type containing t type Two = Foo<{t: boolean}> // boolean, because the generic parameters match, the type corresponding to infer is used type Three = Foo<{a: number, t: () => void}> // () = > void. Generic definitions are subsets of parameters, which are also applicable
infer is used to extract subtypes of satisfied generic types. Many advanced generic tools also skillfully use this method.
5, Generic tools
Partical<T>
The function of this tool is to make all attributes in the generic type optional
type Partial<T> = { [key in keyof T]?: T[P] }
For example, this type definition will also be used below
type Animal = { name: string, category: string, age: number, eat: () => number }
Wrap it with partial
type PartOfAnimal = Partical<Animal>; const ww: PartOfAnimal = { name: 'ww' }; // After all attributes are optional, you can assign only some attributes
Record<K, T>
The function of this tool is to convert all attribute values in K into T type. We often use it to declare a common object object
type Record<K extends keyof any,T> = { [key in K]: T }
In particular, the type corresponding to keyof any is number | string | symbol, which is a collection of types that can be used as object keys (professionally called index).
for instance
const obj: Record<string, string> = { 'name': 'xiaoming', 'tag': 'Three good students' }
Pick<T, K>
The function of this tool is to extract the K key list in T type and generate a new sub key value pair type
type Pick<T, K extends keyof T> = { [P in K]: T[P] }
Let's use the above Animal definition to see how Pick is used
const bird: Pick<Animal, "name" | "age"> = { name: 'bird', age: 1 }
Exclude<T, U>
This tool removes the intersection of T type and U type in T type and returns the rest
type Exclude<T, U> = T extends U ? never : T
Note that the t returned by the extensions here is the attribute that has no intersection between the original T and U, and any attribute union never is itself. Please refer to the above for details.
for instance
type T1 = Exclude<"a" | "b" | "c", "a" | "b">; // "c" type T2 = Exclude<string | number | (() => void), Function>; // string | number
Omit<T, K>
This tool can be considered as an Exclude for key value pair objects. It removes key value pairs containing K in type T
type Omit = Pick<T, Exclude<keyof T, K>>
In the definition, the first step is to remove the key overlapping with K from t's key, and then use Pick to combine T type and the remaining keys
Let's take the above Animal as an example
const OmitAnimal:Omit<Animal, 'name'|'age'> = { category: 'lion', eat: () => { console.log('eat') } }
It can be found that the results obtained by Omit and Pick are completely opposite. One is to take the non result and the other is to take the intersection result.
ReturnType<T>
This tool is to obtain the return value type corresponding to T type (function)
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
In fact, there are a lot of source code. In fact, it can be simplified to the following
type ReturnType<T extends func> = T extends () => infer R ? R: any;
Infer the return value type by using infer, and then return this type. If you fully understand the meaning of infer, this paragraph is very easy to understand
for instance
function foo(x: string | number): string | number { /*..*/ } type FooType = ReturnType<foo>; // string | number
Required<T>
This tool can make all attributes in type T mandatory
type Required<T> = { [P in keyof T]-?: T[P] }
Here is an interesting grammar -?, Can you understand that it is in TS? The meaning of optional attribute subtraction.
In addition to these, there are many built-in type tools for reference TypeScript Handbook For more details, Github also has many third-party type AIDS, such as utility-types Wait.
6, Project practice
Here I share some of my personal thoughts, which may be one-sided or even wrong. You are welcome to leave a message for discussion
Q: interface or type?
A: In terms of usage, there is essentially no difference between the two. If you use React project for business development, it is mainly used to define Props and interface data types.
However, from the perspective of extension, type is more convenient to expand than interface. If there are the following two definitions
type Name = { name: string }; interface IName { name: string };
If you want to extend the type, you only need one &, while the interface needs to write a lot of code
type Person = Name & { age: number }; interface IPerson extends IName { age: number };
In addition, there are some things that the interface cannot do, such as using | to combine enumerated types, using typeof to obtain defined types, and so on.
However, one powerful aspect of the interface is that it can define and add attributes repeatedly. For example, if we need to add a custom attribute or method to the window object, we can directly add attributes based on its interface.
declare global { interface Window { MyNamespace: any; } }
Generally speaking, we all know that TS is type compatible rather than type name matching, so I generally use type to define types when object-oriented scenarios or global types do not need to be modified.
Q: Is any type allowed
A: To tell you the truth, when I first started using TS, I still liked to use any. After all, we all transitioned from JS, and we can't fully accept this code development method that affects efficiency. Therefore, no matter whether it is because of laziness or unable to find a suitable definition, there are many cases of using any.
With the increase of usage time and the deepening of learning and understanding of TS, it is gradually inseparable from the type definition bonus brought by ts. we don't want any in the code. All types must find the corresponding definitions one by one, and even have lost the courage to write JS naked.
This is a question that has no correct answer at present. We should always find the most suitable balance among factors such as efficiency and time. However, I still recommend using TS. with the evolution of front-end engineering and the improvement of its status, strongly typed language must be one of the most reliable guarantees for multi person cooperation and code robustness. It is also a general consensus in the front-end community to use more TS and less any.
Q: How to place a type definition file (. d.ts)
A: It seems that there is no special unified standard in the industry. My thoughts are as follows:
- Temporary type, which is defined directly when used
For example, if you write a Helper inside a component, the input and output parameters of the function are only for internal use, and there is no possibility of reuse. You can directly define it later when defining the function
function format(input: {k: string}[]): number[] { /***/ }
- The component personalization type is directly defined in the ts(x) file
For example, in AntD component design, Props, State s, etc. of each individual component are specifically defined and export ed
// Table.tsx export type TableProps = { /***/ } export type ColumnProps = { /***/ } export default function Table() { /***/ }
In this way, if users need these types, they can use them by importing them.
- Range / global data, defined in d. In TS file
We have no objection to global type data. Generally, there is a types folder under the root directory, which will store some global type definitions.
If we use css module, we need to let TS recognize it After the less file (or. scss) is introduced, it is an object, which can be defined in this way
declare module '*.less' { const resource: { [key: string]: string }; export = resource; }
For some global data types, such as the general data types returned by the back end, I am also used to putting them in the types folder and using Namespace to avoid name conflict, which can save the statements defined by the component import type
declare namespace EdgeApi { export interface Department { description: string; gmt_create: string; gmt_modify: string; id: number; name: string; } }
In this way, each time you use it, you only need const Department: edgeapi Department can save a lot of energy. As long as developers can agree on specifications and avoid naming conflicts.
This concludes the summary of TS usage. Thank you for watching~