Hi, beanskins, this is an advanced use of TypeScript by Bytedancer for those of you who already know about TypeScript or have been using it for a while. This article takes a systematic look at some of the features that TypeScript articles don’t cover in terms of types, operators, operators, and generics.

Author: a young blacksmith in Milan

Type a,

unknown

Unknown refers to a type that cannot be defined in advance. In many scenarios, it can replace the functionality of any while preserving the ability to statically check.

const num: number = 10;
(num as unknown as string).split(' ');          // Note that static checks are just as valid here as any
Copy the code

You can cast it to any type. The difference is that when compiled statically, unknown cannot call any method, whereas any can.

const foo: unknown = 'string';
foo.substr(1);           // Error: Static check fails
const bar: any = 10;
bar.substr(1);                // Pass: any abandons static checking
Copy the code

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

function test(input: unknown) :number {
  if (Array.isArray(input)) {
    return input.length;    // Pass: In this code block, the type guard has recognized the input as an array
  }
  return input.length;      // Error: The input is of unknown type, static check Error. If the entry parameter is any, the check will be aborted and success will be achieved, which brings the risk of an error
}
Copy the code

void

In TS, void has a similar function to undefined, which logically avoids the error of accidentally using a null pointer

function foo() {}          // This empty function returns no value. The default return type is void
const a = foo();        // The type of a is defined as void, and you cannot call any of a's property methods
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 of a function. Take a practical example from React.

// Parent.tsx
function Parent() :JSX.Element {
  const getValue = (): number= > { return 2 };           /* The function returns number */
  // const getValue = (): string => { return 'str' }; /* The string returned by this function can also be passed to the child */
  return <Child getValue={getValue} />
}
Copy the code
// Child.tsx
type Props = {
  getValue: () = > void;  // Void returns number, string, undefined, and so on
}
const Child = ({ getValue }: Props) = > <div onClick={()= > getValue()}>click</div>;
Copy the code

never

Never is a type that cannot be returned normally, as a function that is bound to report an error or an infinite loop returns.

function foo() :never { throw new Error('error message')}// throw error returns 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 types that never intersect

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

But any type combined with the never type is the same type

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

Never has the following features:

  • After you call a function in a function that returns never, the rest of your code becomesdeadcode
function test() {
  foo();                  // foo refers to the function above that returns never
  console.log(111);         // Error: The compiler reported an Error and this line of code will never be executed
}
Copy the code
  • You cannot assign another type to never
let n: never;
const o: any = {};
n = o;  // Error: cannot assign a non-never value to never, including any
Copy the code

There are some hack uses and discussions of never, such as yuxi’s answer under Zhihu

Second, operators

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, with the exclamation mark! After that, TS compiles without error
}
Copy the code

Looking at the compiled ES5 code, there is no air defense judgment

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

This notation scenario is particularly useful for scenarios where we already know that no null value will 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 operators? .

Compared to above! Nonnull judgment at compile time,? This is the run-time (and 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

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

Here’s a quick tip: undefined is reassigned in non-strict mode, and void 0 always returns true undefined

Null-value merge operator??

?? And | | function is similar, the difference is that * *?? ** is returned only when the result of the left-hand expression is null or undefined

For example, we wrote const b = a?? 10, the generated code is as follows

constb = 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

Numeric delimiter _

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

_ can be used for arbitrary separation of long numbers, mainly designed for easy reading of numbers, compiled code is not underlined, please feel free to eat

Operators

Key value gets keyof

Keyof can take 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 the legalization of keys for accessing objects, since any is not acceptable for indexing

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

The syntax for keyof is summarized 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 }  // It 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 left-hand value

const typestr = typeof me;   // Typestr values are "object"
Copy the code

Typeof can be used with keyof (because typeof returns a type) as follows

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

The syntax of typeof is summarized as follows

Type = typeof instance objectCopy the code

Iterate over the property in

In can only be used in the definition of a type, and you can iterate over enumerated types as follows

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

Keyof returns an enumeration of all keys of the generic T. Key is any custom variable name with an in link in the middle and a [] wrapper around it. The number to the right of the colon defines all keys as number.

So you can use it like this

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

The syntax of in is summarized as follows

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

Fourth, generics

Generics are a very important property in TS. They serve as a bridge from static definition to dynamic invocation, as well as the metaprogramming of TS’s own type definitions. Generics are the essence of the TS tool and the most difficult part to learn in TS, which is summarized in two chapters.

The basic use

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

// Plain type definition
type Dog<T> = { name: string.type: T }
// Common type use
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]];
}
// use the function
swipe<Cat<number>, Dog<number>>([cat, dog])  Swipe ([cat, dog])
Copy the code

Note that if you define a generic type for a type name, you must write the generic type as well when using the type name.

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

The syntax of generics is briefly summarized as follows

Type name < generic list > Concrete type definitionCopy the code

Generic derivation with default values

As mentioned above, we can simplify writing generic type definitions because TS automatically deduces the type of the variable from the type at which it was defined. This is typically the case in function calls

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

If function generic derivation is not applicable, we must specify generic types if we need to define variable types

Const dog: dog <string> = {name: 'ww', type: 'HSQ'} // The <string> part cannot be omittedCopy the code

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

type Dog<T = any> = { name: string, type: T } const dog: Dog = { name: 'ww', type: 'hsq' } dog.type = 123; // If the type type is any, it can't be derived automaticallyCopy the code

The syntax format of the generic default values is briefly summarized as follows

Generic name = default typeCopy the code

Generic constraint

Sometimes, we can ignore 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, resulting in an array filled with the default value. We don’t need to judge the parameters passed in and just populate them, but sometimes 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

This allows you to call the summation function as sum([1,2,3]), whereas something like sum([‘1’, ‘2’]) will not compile

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

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

The constraint that U must be a subset of the key types of T is common in some generic libraries.

The syntax of extends is briefly summarized below, noting that the following types can be either generic or generic

The generic name extends typeCopy the code

The generic conditions

Extends, mentioned above, can also be thought of as a ternary operator, as follows

T extends U? X: Y
Copy the code

There is no constraint 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.

Notice that the results generated are distributive.

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

The returned T, in this case, is the part of the original T that contains U, which can be understood as the intersection of T and U

Therefore, the syntactic format of extends can be extended to

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

Generic infer

Infer is usually used with generics conditional statements like the one above. Infer means you do not have to infer from the list of generics but rather from the general structure. For example

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

First look at extends. {t: infer Test} can be thought of as a type definition with a T attribute. The value type of the T attribute will be assigned to the Test type after infer. Infer Test} is used to return Test and default string.

Let me give you an example to help you 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, generic definition is a subset of parameters, also fitCopy the code

Infer is used to derive subtypes from satisfied generic types, and many advanced generics tools use this method subtly.

Generic tools

Partical<T>

What this tool does is make all properties in a generic optional

type Partial<T> = {
        [key 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 Partical

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

Record<K, T>

This tool converts all attributes in K to type T. We use it 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.

For example

const obj: Record<string.string> = { 'name': 'xiaoming'.'tag': 'Merit Student' }
Copy the code

Pick<T, K>

This tool extracts the list of K keys from type T to generate a new subkey value pair type

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

Let’s go back to the Animal definition, and see how Pick can be used

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

Exclude<T, U>

This tool removes the intersection of type T and type U in type T and returns the rest

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

Note that the extends returns T as an attribute that has no intersection with the original T and U, and that any attribute associated with 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 objects. It 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 the key that overlaps with K from the key of T, and then use Pick to combine the T type with the remaining keys

Let’s use Animal as an example

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

Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit Omit

ReturnType<T>

This tool is to get the return value type corresponding to type T (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 bit 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 the return value type by using infer and then returning that type. This should be easy to understand if you understand infer thoroughly

For 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

There’s an interesting syntax here -? What do you mean by TS? The meaning of the optional attribute minus.

There are also a number of built-in typing tools available, including TypeScript Handbook for more detailed information, and third-party typing tools such as Utility-types available on Github.

6. Actual combat of the project

Here to share some of my personal ideas, may be one-sided or even wrong, welcome everyone to actively comment on the discussion

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

A: The React project is used to define Props and interface data types for business development.

However, from an extension point of view, type is easier to extend than interface, given the following two definitions

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

To extend a type, type requires only an ampersand, whereas interface requires a lot more code

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.

However, interface has a relatively powerful place is that it can repeatedly define and add attributes, for example, we need to add a custom attribute or method to the window object, then we can directly add attributes based on its interface.

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

In general, you know that TS is type compatible rather than type name matching, so you don’t usually need to use object-oriented scenarios or scenarios where you don’t need to modify global types. I usually use type to define types.

Q: Whether to allow any type

A: To be honest, WHEN I first started using TS, I liked to use any. After all, people were coming from JS, and they didn’t fully accept this kind of inefficient way of developing code, so I used any A lot, either because I was lazy or because I couldn’t find the right definition.

As the usage time increases and my understanding of TS deepens, I gradually cannot do without the type definition bonus brought by TS. I do not want any to appear in the code. All types have to find the corresponding definition one by one, and I even lose the courage to write JS naked.

This is a question that has no right answer. It’s always about finding a balance between efficiency and time that works best for you. I would recommend TS, however, because as front-end engineering evolves and becomes more important, strongly typed languages are one of the most reliable guarantees of multi-player collaboration and robust code, and it is a general consensus in the front-end world to use TS more than ANY.

Q: Where are the type definition files (.d.ts) placed

A: It seems that there is no uniform standard in the industry. My thoughts are as follows:

  • Temporary type, defined directly at use time

For example, if you write an internal Helper for a component, the input and output parameters of a function are for internal use only and cannot be reused. You can define the parameters later when defining the function

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

For AntD component design, Props, State, and so on for each individual component are specifically defined and exported

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

This allows users to import these types if they need them.

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

Global type data, which everyone agrees with, usually has a typings folder in the root directory that holds some global type definitions.

If we are using CSS Modules, then we need to let TS recognize that the.less file (or.scSS) is introduced as an object and can be defined as such

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

For some global data types, such as generic data types returned from the back end, I also prefer to put them in the Typings folder and use Namespace to avoid name collisions, thus saving component import type definition statements

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

This saves a lot of import effort by requiring only const department: edgeapi.department each time it is used. Developers only need to agree on specifications to avoid naming conflicts.

This concludes the summary of TS usage. Thank you for watching

Data platform front-end team, responsible for the research and development of big data-related products such as Fengshen, TEA, Libra and Dorado. We have a strong passion in front-end technology. In addition to research and development related to data products, we have a lot of exploration and accumulation in data visualization, mass data processing optimization, Web Excel, WebIDE, private deployment and engineering tools. If you are interested, please feel free to contact us.