This article mainly introduces the advanced use of TypeScript, for TypeScript already have some understanding or have actually used for a period of time students, respectively, from the type, operators, operators, generics to systematically introduce the common TypeScript article did not explain the function points, and then finally share their own practical experience.
I. Types
unknown
unknown refers to a type that cannot be predefined, and in many scenarios it can replace the functionality of any while retaining the ability to check statically.
const num: number = 10;
(num as unknown as string).split('');
At this point, unknown is highly similar to any, in that you can convert it to any type, except that unknown can’t call any methods when compiled statically, whereas any can.
const foo: unknown = 'string';
foo.substr(1);
const bar: any = 10;
any.substr(1);
One use case for unknown is to avoid the static type-checking bug caused by using any as the argument type of a function:
function test(input: unknown): number {
if (Array.isArray(input)) {
return input.length;
}
return input.length;
}
void
In TS, void and undefined are highly similar in function and can logically avoid errors caused by accidental use of null pointers.
function foo() {}
const a = foo();
The biggest difference between the void and undefined types is that you can think of undefined as a subset of void, and when you don’t care about the return value of a function, use void instead of undefined. give a real-world example from React.
// Parent.tsx
function Parent(): JSX.Element {
const getValue = (): number => { return 2 };
// const getValue = (): string => { return 'str' };
return <Child getValue={getValue} />
}
// Child.tsx
type Props = {
getValue: () => void;
}
function Child({ getValue }: Props) => <div>{getValue()}</div>
never
never is the type that is not returned by normal termination; a function that is bound to report an error or a dead loop will return such a type.
function foo(): never { throw new Error('error message') }
function foo(): never { while(true){} }
function foo(): never { let count = 1; while(count){ count ++; } }
Then there are the types that never intersect:
type human = 'boy' & 'girl'
But any type united with a never type is still the same type:
type language = 'ts' | never
Never has the following properties:
After calling a function that returns never in a function, all subsequent code becomesdeadcode
function test() {
foo();
console.log(111);
}
- It is not possible to assign other types to never:
let n: never;
let o: any = {};
n = o;
There are some hacky uses and discussions about this feature of never, such as this answer from You Yuxi on Zhihu.
II. Operators
The non-empty assertion operator !
This operator can be used after a variable or function name to emphasize that the corresponding element is not null|undefined.
function onClick(callback?: () => void) {
callback!();
}
You can look at the compiled ES5 code, and surprisingly, it doesn’t make any anti-voiding judgments.
function onClick(callback) {
callback();
}
This notation is particularly useful for scenarios where we already know explicitly that we won’t return null values, thus reducing redundant code judgments, such as React’s Ref.
function Demo(): JSX.Elememt {
const divRef = useRef<HTMLDivElement>();
useEffect(() => {
divRef.current!.scrollIntoView();
}, []);
return <div ref={divRef}>Demo</div>
}
Optional chain operator ?.
Compared to the above! which works at compile time, ?.
is the most important runtime (and of course compile-time) non-null judgment that developers need.
obj?.prop obj?.[index] func?.(args)
?. is used to determine if the expression on the left is null | undefined, and if it is, it stops the expression from running, which reduces the amount of && operations we have to perform.
For example, when we write a?.b
, the compiler automatically generates the following code
a === null || a === void 0 ? void 0 : a.b;
Here’s a little bit of trivia: the value undefined
is reassigned in non-strict mode, and using void 0
must return a true undefined.
Null merge operator ??
?? is similar to ||, except that ?? returns the right-hand expression only if the left-hand expression results in null or undefined.
For example, if we write let b = a ?? 10
, the generated code is as follows:
let b = a !== null && a !== void 0 ? a : 10;
The || expression, as we all know, will also work on logical nulls such as false, ”, NaN, 0, etc., which is not suitable for merging parameters.
Number separator _
let num:number = 1_2_345.6_78_9
_ 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 feel free to eat.
III. Operators
Get 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;
A typical use of keyof is to restrict access to the key legalization of an object, since any is not accepted for indexing.
function getValue (p: Person, k: keyof Person) {
return p[k];
}
To summarize the syntax of keyof, here is the format
Instance type gets typeof
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 }
typeof can only be used on concrete objects, which is consistent with typeof in js, and it automatically decides which behavior to perform based on the left-hand side value.
const typestr = typeof me;
typeof can be used with keyof (since typeof returns a type) as follows:
type PersonKey = keyof typeof me;
The syntax of typeof is summarized as follows:
Iterate over the attributes in
in can only be used in the definition of types that can be traversed for enumerated types, as follows:
type TypeToNumber<T> = {
[key in keyof T]: number
}
keyof
Returns all key enumeration types of the generic T. key
is customized with any variable name, linked in the middle with in
and wrapped around the periphery with []
(this is a fixed collocation), and number
on the right side of the colon defines all key
as being of type number
.
So it can be used like this:
const obj: TypeToNumber<Person> = { name: 10, age: 10 }
The syntax of in is summarized as follows:
IV. Generalizations
Generics are a very important attribute in TS, as they bridge the gap from static definitions to dynamic invocations, and they are also the meta-programming of TS’s own type definitions. Generics can be said to be the essence of the TS type tools, but also the most difficult part of the whole TS to learn, here is a summary of two chapters.
Basic use
Generics can be used for general type definitions, class definitions, and function definitions as follows:
type Dog<T> = { name: string, type: T }
const dog: Dog<number> = { name: 'ww', type: 20 }
class Cat<T> {
private type: T;
constructor(type: T) { this.type = type; }
}
const cat: Cat<number> = new Cat<number>(20);
function swipe<T, U>(value: [T, U]): [U, T] {
return [value[1], value[0]];
}
swipe<Cat<number>, Dog<number>>([cat, dog])
Note that if a generic type is defined for a type name, then when using this type name be sure to write the generic type as well.
And for variables, you can omit the generic writing if its type can be inferred at call time.
The syntax format of a generic is briefly summarized below:
Generic Derivation and Default Values
As mentioned above, we can simplify the writing of generic type definitions because TS automatically deduces the variable type based on the type of the variable at the time of its definition, which generally occurs in the context of 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);
If the function generic derivation does not apply, we must specify the generic type if we need to define the variable type.
const dog: Dog<string> = { name: 'ww', type: 'hsq' }
If we want to leave it unspecified, we can use the generalized default value scheme.
type Dog<T = any> = { name: string, type: T }
const dog: Dog = { name: 'ww', type: 'hsq' }
dog.type = 123;
The syntax format for generic default values is briefly summarized below:
generalized constraint
There are times when we can get away with not focusing on the specific type of the generalization, such as:
function fill<T>(length: number, value: T): T[] {
return new Array(length).fill(value);
}
This function takes a length parameter and a default value, and the result is an array that is filled with the corresponding number of items using the default value. We don’t have to judge the parameters passed in, we just fill them in, but sometimes we need to qualify the type, which can be done with the extends
keyword.
function sum<T extends number>(value: T[]): number {
let count = 0;
value.forEach(v => count += v);
return count;
}
This way you can call the summing function as sum([1,2,3])
, whereas something like sum(['1', '2'])
won’t pass compilation.
Generic constraints can also be used in the case of multiple generic parameters.
function pick<T, U extends keyof T>(){};
The idea is to restrict U to be a subset of the key type of T. This usage is often found in generalization libraries.
The syntax of extends is briefly summarized as follows. Note that the following types can be both general and generic.
generalized condition (math.)
The reference to extends above can actually be treated as a ternary operator, as follows:
T extends U? X: Y
There is no restriction that T must be a subtype of U. If it is a subtype of U, then T is defined as type X; otherwise, it is defined as type Y.
Note that the generated result is distributive.
For example, if we replace X with T of this form: T extends U? T: never
.
At this point, the returned T, is to meet the original T contains U part, can be understood as T and U intersection.
Therefore, the syntax of extends can be extended to
Generalized infer infer
infer is the Chinese meaning of “infer”, generally used with the above generalized conditional statements, the so-called infer, that is, you do not have to pre-specify in the list of generalized, will be automatically judged at runtime, but you have to predefine the overall structure. As an example.
type Foo<T> = T extends {t: infer Test} ? Test: string
The first choice is to look at the content after the extends, {t: infer Test}
can be seen as a type definition containing t
, the value type of this t
will be assigned to the type of Test
after being inferred from infer
, if the actual parameter of the generalized type conforms to the definition of {t: infer Test}
, then the return is the type of Test
, otherwise it will be given by default to the type of string
.
Give me an example to deepen your understanding:
type One = Foo<number>
type Two = Foo<{t: boolean}>
type Three = Foo<{a: number, t: () => void}>
infer
is used to subtype a satisfied generic type, and there are a number of advanced generic tools that make clever use of this method.
V. Generalization tools
Partial <T>
The purpose of this tool is to make all attributes in a generic type optional.
type Partial<T> = {
[P in keyof T]?: T[P]
}
As an example, this type definition is also used below.
type Animal = {
name: string,
category: string,
age: number,
eat: () => number
}
Wrap it up in a Partial.
type PartOfAnimal = Partial<Animal>;
const ww: PartOfAnimal = { name: 'ww' };
Record <K, T>
The purpose of this tool is to convert all attribute values in K to T-types, which are often used to assert a normal object object.
type Record<K extends keyof any,T> = {
[key in K]: T
}
In particular, keyof any
corresponds to number | string | symbol
, which is a collection of types that can be used as an object key (index index, in technical terms).
An example:
const obj: Record<string, string> = { 'name': 'zhangsan', 'tag': '2' }
Pick <T, K>
The purpose of this tool is to extract the list of K-keys from the T-type and generate a new type of subkey-value pair.
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}
We’ll still use the Animal
definition above and see how Pick is used.
const bird: Pick<Animal, "name" | "age"> = { name: 'bird', age: 1 }
Exclude<T, U>
This tool removes the intersection of type T and type U in type T and returns the remainder.
type Exclude<T, U> = T extends U ? never : T
Note that the T returned by extends here is the attribute of the original T that does not intersect with U, whereas any attribute union never is itself, as can be seen above.
give an example
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 thought of as Exclude for key-value pair objects, which removes key-value pairs of type T that contain K.
type Omit = Pick<T, Exclude<keyof T, K>>
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 type of T with the remaining key.
Let’s stick with the Animal example from above:
const OmitAnimal:Omit<Animal, 'name'|'age'> = { category: 'lion', eat: () => { console.log('eat') } }
It can be noticed that Omit and Pick get completely opposite results, one takes the non-results and the other takes the intersection results.
ReturnType<T>
This tool is to get the type of return value corresponding to a T-type (function):
type ReturnType<T extends (...args: any) => any>
= T extends (...args: any) => infer R ? R : any;
Looking at the source code is actually a bit much, but it can actually be simplified slightly to the following:
type ReturnType<T extends func> = T extends () => infer R ? R: any;
Inferring the return value type by using infer, and then returning that type, is a good paragraph if you thoroughly understand what infer means.
An example:
function foo(x: string | number): string | number { /*..*/ }
type FooType = ReturnType<foo>; // string | number
Required<T>
This tool makes all attributes of type T mandatory.
type Required<T> = {
[P in keyof T]-?: T[P]
}
There is an interesting syntax here -?
which you can understand to mean that it is the TS that subtracts the ? in TS to subtract the optional attributes.
In addition to these, there are a number of built-in type tools, which you can refer to the TypeScript Handbook for more detailed information, as well as a number of third-party type assistants on Github, such as utility-types.
VI. Project practice
Here to share some of my personal ideas, may perhaps be more one-sided or even wrong, welcome to actively leave a message to discuss!
Q: Do you prefer interface or type for defining types?
A: In terms of usage, there is essentially no difference between the two. If you are using a React project for business development, it is mainly used to define Props and interface data types.
However, from an extensibility point of view, type is a bit easier to extend than interface, if you have the following two definitions:
type Name = { name: string };
interface IName { name: string };
If you want to do type extensions, type only needs a &
, whereas interface requires a lot more code.
type Person = Name & { age: number };
interface IPerson extends IName { age: number };
In addition, type does some things that interface can’t, such as combining enumerated types using |
, getting defined types using typeof
, and so on.
However, one of the more powerful aspects of interfaces is the ability to add properties over and over again. For example, if we need to add a custom property or method to the window
object, then we can just add a new property based on its Interface.
declare global {
interface Window { MyNamespace: any; }
}
In general, you know that TS is type compatible rather than type name matching, so I generally use type to define types in scenarios that don’t require object oriented use or don’t need to modify global types.
Q: Is the type any allowed?
A: To be honest, when I first started using TS, I still liked to use any. After all, we are all transitioning from JS, and we can’t fully accept this kind of code development that affects the efficiency, so whether it’s out of laziness or not being able to find a proper definition, there are more cases of using any.
With the increase in the use of time and the deepening of the understanding of TS, gradually away from the type definition bonus brought by TS, do not want to appear in the code any, all the types must be one by one to find the corresponding definition, and even have lost the courage to write bare JS.
This is a question that has no right answer at the moment, it’s always a matter of finding the best balance between efficiency and time and other factors. However, I still recommend using TS. As front-end engineering evolves and grows in stature, a strongly typed language must be one of the most reliable guarantees of multi-person collaboration and code robustness, and there is a general consensus in the front-end community to use more TS and less any.
Q: How to place type definition files (.d.ts)
A: There seems to be no standardization in the industry, my thoughts are as follows:
- Temporary types, defined directly at the time of use
If you write your own Helper inside a component, the in- and out-references of the function are only for internal use and there is no possibility of reuse, so you can define them directly in the back of the function definition.
function format(input: {k: string}[]): number[] { /***/ }
- Component personalization types, defined directly in the ts(x) file
As in the AntD component design, the Props, States, etc. of each individual component are specifically defined and exported.
// Table.tsx
export type TableProps = { /***/ }
export type ColumnProps = { /***/ }
export default function Table() { /***/ }
This way, users who need these types can use them by importing them.
- Scope/global data, defined in .d.ts file
Global type data, there is no doubt that there is a typings folder in the root directory that holds some global type definitions.
If we are using the css module, then we need to make TS recognize that the .less file (or .scss) is an object when introduced, which can be defined like this:
declare module '*.less' {
const resource: { [key: string]: string };
export = resource;
}
For global datatypes, such as generic datatypes returned by the backend, I also used to put them in the typings folder and use Namespace to avoid name clashes, which saves the component import typedef statements.
declare namespace EdgeApi {
interface Department {
description: string;
gmt_create: string;
gmt_modify: string;
id: number;
name: string;
}
}