TypeScript advanced usage

Posted by lutzlutz896 on Thu, 03 Mar 2022 08:10:12 +0100

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~

The End

Topics: Front-end TypeScript