This article introduces the advanced usages of TypeScript. It is intended for those who already know TypeScript or have used TypeScript for some time. It provides a systematic introduction to TypeScript’s types, operators, operators, and generics. Finally, I would like to share my experience in practice.

Type a,

unknown

Unknown is a type that cannot be defined in advance. In many scenarios, it can replace any functionality while retaining static checking.

const num: number = 10;
(num as unknown as string).split(' ');  	// Note that this is the same as any that passes static checking
Copy the code

You can convert it to any type. The difference is that during static compilation, unknown cannot call any method, whereas any does.

const foo: unknown = 'string';
foo.substr(1);   	// Error: static check failed
const bar: any = 10;
any.substr(1);		// The Pass: any type is equivalent to abandoning static checking
Copy the code

One use case for unknown is to avoid static type checking bugs caused by using any as the argument type of a function:

function test(input: unknown) :number {
  if (Array.isArray(input)) {
    return input.length;    // Pass: This is a block of code where the type guard has identified input as an array type
  }
  return input.length;      // Error: The input type is unknown. If the input parameter is any, the check will be abandoned and directly successful, with the risk of an error
}
Copy the code

void

In TS, the void and undefined functions are highly similar, logically avoiding the error of accidentally using null Pointers.

function foo() {}  	// This empty function returns no value. The default return type is void
const a = foo();	// If a is of type void, you cannot call any of the property methods of a
Copy the code

The biggest difference between void and undefined is that you can think of undefined as a subset of void, and use void instead of undefined when you don’t care about the return value. Let’s take an actual example from React.

// Parent.tsx
function Parent() :JSX.Element {
  const getValue = (): number= > { return 2 };   	/* The function returns type */
  // const getValue = (): string => { return 'str' }; /* This function returns a string, which can also be passed to a child property */
  return <Child getValue={getValue} />
}
Copy the code
// Child.tsx
type Props = {
  getValue: () = > void;  // The return type of the return value is undefined. The return type of the return value is undefined
}
function Child({ getValue }: Props) = > <div>{getValue()}</div>

Copy the code

never

Never is a type that does not end properly, as a function that is bound to report an error or an infinite loop would return.

function foo() :never { throw new Error('error message')}// Throw error returns a value of never
function foo() :never { while(true){} }  // The loop will not exit properly
function foo() :never { let count = 1; while(count){ count ++; }}// Error: This cannot define the return value as never, because it cannot be recognized directly during static compilation
Copy the code

And then there are the types that never intersect:

type human = 'boy' & 'girl' // The two separate string types cannot intersect, so human is of type never
Copy the code

However, if any type is associated with never, it will still be the same type:

type language = 'ts' | never   // Language is still of type 'ts'
Copy the code

The following features about never are available:

  • After a function that returns never is called inside a function, all subsequent code becomesdeadcode
function test() {
  foo();  		// Foo refers to the function that returns never
  console.log(111); 	// Error: The compiler reports an Error, and this line of code will never be executed
}
Copy the code
  • You cannot assign another type to never:
let n: never;
let o: any = {};
n = o;  // Error: cannot assign a non-never type to never, including any
Copy the code

There are some hacky uses and discussions about this feature of never, such as this zhihuxia answer by Yu Yuxi.

Operator

Non-empty assertion operator!

The operator can be used in a variable name or the function name, used to highlight the corresponding element is null | undefined

function onClick(callback? : () = >void) { callback! (a);// The argument is optional. After that, TS compiles without error
}
Copy the code

You can look at the compiled ES5 code and see no anti-aircraft judgment.

function onClick(callback) {
  callback();
}
Copy the code

This symbol scenario is especially useful for scenarios where we already know that the null value will not be returned, thus reducing redundant code judgments, such as the React Ref.

function Demo() :JSX.Elememt {
  const divRef = useRef<HTMLDivElement>();
  useEffect(() = >{ divRef.current! .scrollIntoView();UseEffect is triggered when the component is mounted, so current must have a value} []);return <div ref={divRef}>Demo</div>
}
Copy the code

Optional chain operator? .

Compared to above! Non-null judgment applied at compile time,? This is the run-time (and, of course, compile-time) non-null judgment that developers need most.

obj? .prop obj? .[index] func? .(args)Copy the code

? . Used to judge whether the left side of the expression is null | undefined, if it’s expression will stop running, can reduce a lot of our && operations.

So let’s say we write a, right? . B, the compiler automatically generates the following code

a === null || a === void 0 ? void 0 : a.b;
Copy the code

A small point here is that undefined is reassigned in non-strict mode, so void 0 must return the true undefined.

Null merge operator??

?? And | | function is similar, the difference is that?? The right expression is returned only if the left expression results in null or undefined.

Let’s say we write let b = a?? 10. The generated code is as follows:

letb = a ! = =null&& a ! = =void 0 ? a : 10;
Copy the code

The | | expression, you know,, ‘, NaN, 0 for false logic null will take effect, such as of the merger of the parameters is not suitable for us to do.

The number separator _

let num:number = 1 _2_345. 6 _78_9
Copy the code

_ can be used to do arbitrary separation of long numbers, the main design is to facilitate the reading of numbers, compiled code is not underlined, please rest assured to eat.

Operator

Gets the keyof

Keyof can get all the keys of a type and return a union type, as follows:

type Person = {
  name: string;
  age: number;
}
type PersonKey = keyof Person;  / / PersonKey get type for 'name' | 'age'
Copy the code

A typical use of keyof is to restrict access to objects by legitimizing keys, since any indexing is not acceptable.

function getValue (p: Person, k: keyof Person) {
  return p[k];  // If k is not so defined, it cannot be compiled in p[k] code format
}
Copy the code

To summarize, the syntax of keyof is as follows

Type = Keyof typeCopy the code

Instance type Gets Typeof

Typeof is the type that gets 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
Copy the code

Typeof can only be used on concrete objects, which is consistent with typeof in JS, and it automatically determines which behavior should be performed based on the values on the left.

const typestr = typeof me;   // The value of typestr is "object"
Copy the code

Typeof can be used with keyof (since typeof returns a type), as follows:

type PersonKey = keyof typeof me;   // 'name' | 'age'
Copy the code

In summary, the syntax of Typeof is as follows:

Type = Typeof instance objectCopy the code

Traversal attribute in

In can only be used in the definition of a type. You can iterate over enumerated types as follows:

// This type can convert any type of key to the type number
type TypeToNumber<T> = {
  [key in keyof T]: number
}
Copy the code

Keyof returns all key enumerations of the generic type T. Key is any custom variable name linked with in and wrapped around [] (this is a fixed collocation). Number to the right of the colon defines all keys to be of type number.

So you can use it like this:

const obj: TypeToNumber<Person> = { name: 10.age: 10 }
Copy the code

To sum up, the syntax of in is as follows:

[Custom variable name in enumeration type]: typeCopy the code

Fourth, generics

Generics is a very important property in TS, as it serves as a bridge from static definition to dynamic invocation, as well as a metaprogramming of TS’s own type definitions. Generics are arguably the heart and soul of t-type tools, and the hardest part of all TS to learn, which is summarized in two chapters.

The basic use

Generics can be used for common type definitions, class definitions, and function definitions, as follows:

// Common type definitions
type Dog<T> = { name: string.type: T }
// Use common types
const dog: Dog<number> = { name: 'ww'.type: 20 }

/ / the class definition
class Cat<T> {
  private type: T;
  constructor(type: T) { this.type = type; }}/ / class
const cat: Cat<number> = new Cat<number> (20); 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])  Swipe ([cat, dog])
Copy the code

Note that if a generic type is defined for a type name, be sure to include the generic type when using the type name.

For variables whose types can be inferred at call time, generic writing can be omitted.

The syntax of generics is summarized as follows:

Type name < generic list > Concrete type definitionCopy the code

Generics derivation and default values

As mentioned above, we can simplify writing generic type definitions because TS automatically deduces the type of a variable from the type at which it was defined, which usually happens when a function is called.

type Dog<T> = { name: string, type: T } function adopt<T>(dog: Dog<T>) { return dog }; const dog = { name: 'ww', type: 'hsq' }; Adopt (Dog); // Adopt (Dog); // Adopt (Dog); // Pass: the function will infer that type is string based on the type of the input parameterCopy the code

If function generics derivation is not applicable, we must specify the generic type if we want to define a variable type.

Const dog: dog <string> = {name: 'ww', type: 'HSQ'Copy the code

If we do not want to specify, we can use the scheme with generic defaults.

type Dog<T = any> = { name: string, type: T } const dog: Dog = { name: 'ww', type: 'hsq' } dog.type = 123; // In this case, the type type is any, which is not automatically derived, and loses its generic meaningCopy the code

A simple summary of the syntax for generic defaults is as follows:

Generic name = Default typeCopy the code

Generic constraint

Sometimes we don’t have to worry about the specific types of generics, such as:

function fill<T> (length: number, value: T) :T[] {
  return new Array(length).fill(value);
}
Copy the code

This function takes a length argument and a default value, and the result is an array populated with the default values. We don’t need to make a judgment about the parameters passed in, we just fill them in, but there are times when we need to qualify the type and use the extends keyword.

function sum<T extends number> (value: T[]) :number {
  let count = 0;
  value.forEach(v= > count += v);
  return count;
}
Copy the code

So you can call the sum function as sum([1,2,3]), whereas something like sum([‘1’, ‘2’]) doesn’t compile.

Generic constraints can also be used in the case of multiple generic parameters.

function pick<T.U extends keyof T> (){};
Copy the code

This means that U must be a subset of the key type of T, and this usage is often found in some generic libraries.

The syntax format of extends is briefly summarized below. Note that the following types can be either generic or generic.

The generic name extends typeCopy the code

The generic conditions

The extends operator can also be used as a ternary operator, as follows:

T extends U? X: Y
Copy the code

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 results generated are distributive.

For example, if we replace X with T, the form is: T extends U? T: never.

The T that is returned is the part of T that satisfies the original T that contains U, which can be understood as the intersection of T and U.

So, the syntax format of extends can be extended to

Generic name A extends type B? Type C: Type DCopy the code

The general type is infer

Infer means’ to infer ‘, which is usually used with generic conditions like the one above. By infer, you don’t have to specify it in a generic list. For example

type Foo<T> = T extends {t: infer Test} ? Test: string
Copy the code

The extends {t: infer Test} can be viewed as a type definition that includes the T attribute. The T attribute’s value type is assigned to the Test type by infer. Infer Test} then returns the Test type. Otherwise, the infer type is default string.

Here’s an example to further understand:

Type One = Foo<number> // string, because number is not an object type containing t. Type Two = Foo<{t: Infer = type type Three = Foo<{a: number, t: () => void}> // () => void, the generic definition is a subset of the arguments, which also fitsCopy the code

Infer can be used to extract subtypes of the desired generic type. Many advanced generic tools also use this method subtly.

5. Generic tools

Partial<T>

What this tool does is make all properties in generics optional.

type Partial<T> = {
	[P inkeyof T]? : T[P] }Copy the code

For example, this type definition is also used below.

type Animal = {
  name: string.category: string.age: number.eat: () = > number
}
Copy the code

Wrap it with Partial.

type PartOfAnimal = Partial<Animal>;
const ww: PartOfAnimal = { name: 'ww' }; // When all attributes are optional, you can assign only some attributes
Copy the code

Record<K, T>

This tool converts all property values in K to type T. It is often used to declare a normal object.

type Record<K extends keyof any,T> = {
  [key in K]: T
}
Copy the code

Special note here, keyof any corresponding types for the number of | string | symbol, namely can do object keys (professional term is index index) the type of collection.

Here’s an example:

const obj: Record<string.string> = { 'name': 'zhangsan'.'tag': 'Working man' }
Copy the code

Pick<T, K>

The purpose of this tool is to extract the list of K keys from the T type and generate a new subkey value pair type.

type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
}
Copy the code

Let’s use that Animal definition again, and let’s see how Pick works.

const bird: Pick<Animal, "name" | "age"> = { name: 'bird'.age: 1 }
Copy the code

Exclude<T, U>

This tool returns the remainder of a T type that removes the intersection of the T and U types.

type Exclude<T, U> = T extends U ? never : T
Copy the code

Note that the extends returns T as a property with no intersection between the original T and U, and any property union of never is itself, as you can see above.

For example

type T1 = Exclude<"a" | "b" | "c"."a" | "b">;   // "c"
type T2 = Exclude<string | number | (() = > void), Function>; // string | number
Copy the code

Omit<T, K>

This tool can be thought of as a Exclude for key-value pairs, which removes key-value pairs of type T that contain K.

type Omit = Pick<T, Exclude<keyof T, K>>
Copy the code

In the definition, the first step is to remove keys that overlap K from T’s keys, and then use Pick to combine the remaining keys with T’s type.

Let’s use Animal as an example:

const OmitAnimal:Omit<Animal, 'name'|'age'> = { category: 'lion'.eat: () = > { console.log('eat')}}Copy the code

It can be found that the results of Omit and Pick are completely opposite, one is non-omit and the other is occlusion.

ReturnType<T>

This tool is to get the type of the return value corresponding to the T type (function) :

type ReturnType<T extends(... args:any) = >any>
  = T extends(... args:any) => infer R ? R : any;
Copy the code

Look at the source code is actually a little more, in fact, can be slightly simplified into the following appearance:

type ReturnType<T extends func> = T extends () => infer R ? R: any;
Copy the code

Infer and return the type of the return value by using infer. This should be easy to understand if you’ve thoroughly understood what infer means.

Here’s an example:

function foo(x: string | number): string | number { /*.. */ } type FooType = ReturnType<foo>; // string | numberCopy the code

Required<T>

This tool makes all properties of type T mandatory.

type Required<T> = {
  [P inkeyof T]-? : T[P] }Copy the code

Here’s an interesting syntax -? You can interpret it as t sub s. Optional attribute subtractive meaning.

In addition to these, there are many built-in typing tools, such as the TypeScript Handbook for more detailed information, as well as third-party typing AIDS such as Utility-types on Github.

Six, project actual combat

Here to share some of my personal ideas, may be one-sided or even wrong, welcome to leave a positive message to discuss

Q: Do you prefer to use interface or type to define a type?

A: There’s basically no difference in usage. If you’re using the React project for business development, it’s basically to define Props and interface data types.

However, from an extension point of view, type is easier to extend than interface if there are two definitions:

type Name = { name: string };
interface IName { name: string };
Copy the code

If you want to extend a type, you only need an & for type, but you need to write a lot more code for interface.

type Person = Name & { age: number };
interface IPerson extends IName { age: number };
Copy the code

In addition type have some interface could not do, such as using | for the combination of enumerated types, using typeof get defined type and so on.

If we want to add a custom property or method to the window object, we can just add the property based on its interface.

declare global { interface Window { MyNamespace: any; }}Copy the code

In general, TS is known to be type-compatible rather than type-name matching, so I usually use type when I don’t need to use object-oriented scenarios or when I don’t need to change the global type.

Q: Whether the any type is allowed

A: To tell you the truth, I liked to use any when I first started to use TS. After all, people are transitioning from JS and are not fully comfortable with this method of code development, which affects the efficiency. Therefore, I used any more often, either out of laziness or because I can’t find A proper definition.

With the increase of use time and the deepening of the understanding of TS learning, the bonus of type definition brought by TS is gradually inseparable. I don’t want any to appear in the code, and all types have to find the corresponding definition one by one. Even I have lost the courage to write JS naked.

This is a question that has no right answer. It is always a matter of finding the right balance between efficiency and time, etc. However, I recommend using TS. As front-end engineering evolves and becomes more important, a strongly typed language must be one of the most reliable guarantees of multi-person collaboration and robust code. It is a general consensus in the front-end industry to use TS more and use ANY less.

Q: How to place type definition files (.d.ts)

A: There doesn’t seem to be A uniform standard in the industry. My thoughts are as follows:

  • A temporary type, defined directly at use

If you write an internal Helper for a component, the incoming and outgoing parameters of the function are only for internal use and there is no possibility of reuse. You can directly define the function when you define it.

function format(input: {k: string} []) :number[] { / * * * / }
Copy the code
  • Component personalization types, defined directly in ts(x) files

For example, in the AntD component design, the Props and State types of each individual component are specifically defined and exported.

// Table.tsx
export type TableProps = { / * * * / }
export type ColumnProps = { / * * * / }
export default function Table() { / * * * / }
Copy the code

In this way, users can import and use these types if they need them.

  • Scope/global data, defined in.d.ts files

Global type data, everyone agrees with that, but there’s usually a typings folder in the root directory, where you have some global type definitions.

If we use a CSS module, then we need TS to recognize the.less file (or.scss) as an object after it is introduced, which can be defined like this:

declare module '*.less' {
  const resource: { [key: string] :string };
  export = resource;
}
Copy the code

For some global data types, such as the generic data types returned by the backend, I also like to put them in the Typings folder, using Namespace to avoid name conflicts, so as to save component import type definition statements.

declare namespace EdgeApi {
  interface Department {
    description: string;
    gmt_create: string;
    gmt_modify: string;
    id: number;
    name: string; }}Copy the code

In this way, each time we use it, we only need const Department: edgeapi. department, which saves a lot of importing effort. Developers just need to be able to agree on specifications to avoid naming conflicts.

The summary of TS usage is introduced here, thank you for watching ~


Welcome to “ByteFE”

Resume delivery email: [email protected]