Speak ahead
本文难度偏中下,涉及到的点大多为如何在项目中合理应用ts,小部分会涉及一些原理,受众面较广,有无TS基础均可放心食用。 **>>>> 阅完本文,您可能会收获到<<<<**
- If you are not familiar with TS, this article can help you complete the learning of TS application part, accompanied by many Demo examples to guide business applications;
- If you are more familiar with TS, then this article can be used as a review article to take you back to your knowledge, hoping to trigger your new discoveries and thoughts at certain points;
- For the IState and IProps of the class component, analogy part of the writing and thinking of the Hook component;
🌟🌟🌟TIPS: Super easy to use online TS editor (many configuration items can be configured manually) Portal: TS online 🌟🌟🌟
1. What is TS
Not to mention the obscure concept, in general, TypeScript is a superset of JavaScript. It has optional types and can be compiled into pure JavaScript to run. (The author has always regarded TypeScript as the Lint of JavaScript) So the question is, why must TS be designed to be static? Or in other words, why do we need to add type specifications to JavaScript?
Classic self-question and self-answer session-because it can solve some unresolved pain points of JS:
- JS is a dynamically typed language, which also means that we don't know the type of the variable before instantiation, but using TS can avoid classic low-level errors before running. Example: Uncaught TypeError:'xxx' is not a function
⚠️ Classical classic level error 🌰:
JS is like this. Only when an error occurs at runtime does it tell me that there is an error, but when TS intervenes:
Good guys! Throw the problem directly in the editor stage, nice!
- Lazy people carnival. The specification is convenient and not easy to make mistakes. For VS Code, the most it can do is to indicate whether there is this attribute, but it cannot accurately indicate what type of this attribute is. However, TS can be deduced/reversed by type (in the vernacular: If you do not write the type explicitly, you will use type inference to infer the type you are using), which perfectly optimizes the code completion:
first Q&A-thinking:
So what can we think of the pain points of JS that TS solves in business development? (Question)
Answer, summarize, add:
-Restrictions on the types of function parameters;
-Restrict the types of arrays and objects to avoid definition errors, such as when data deconstruction is complex or too large,
There may be an array definition error a = {}, if (a.length){ // xxxxx}
-let functionA ='jiawen' // Actually let functionA: string ='jiawen'
- Make our application code easier to read and maintain. If the definition is perfect, the function of the parameter can be roughly understood through the type;
believe that through the above simple bug-demo, you have a preliminary re-understanding of TS
next chapter will formally introduce how we use TS in the business development process
2. How to use TS
在业务中如何用TS/如何用好TS?这个问题其实和 " 在业务中怎么用好一个API " 是一样的。首先要知道这个东西在干嘛,参数是什么,规则是什么,能够接受有哪些扩展......等等。 简而言之,撸它!
TS common types summary
Through a comprehensive summary of common TS errors in the business, I hope that Demos will benefit you
String number boolean of primitives
笔者把基本类型拆开的原因是: 不管是中文还是英文文档,primitives/元语/元组 这几个名词都频繁出镜,笔者理解的白话:希望在类型约束定义时,使用的是字面量而不是内置对象类型,官方文档:
let a: string = 'jiawen';
let flag: boolean = false;
let num: number = 150
interface IState: {
flag: boolean;
name: string;
num: number;
}
Tuple
// 元组类型表示已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。
let x: [string, number];
x = ['jiawen', 18]; // ok
x = [18, 'jiawen']; // Erro
console.log(x[0]); // jiawen
undefined null
let special: string = undefined
// 值得一提的是 undefined/null 是所有基本类型的子类,
// 所以它们可以任意赋值给其他已定义的类型,这也是为什么上述代码不报错的原因
object and {}
// object 表示的是常规的 Javascript对象类型,非基础数据类型
const offDuty = (value: object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // Error
// {} 表示的是 非null / 非undefined 的任意类型
const offDuty = (value: {}) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // ok
// {} 和Object几乎一致,区别是Object会对Object内置的 toString/hasOwnPreperty 进行校验
const offDuty = (value: Object) => {
console.log("value is ", value);
}
offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // Error
如果需要一个对象类型,但对属性没有要求,建议使用 object
{} 和 Object 表示的范围太大,建议尽量不要使用
object of params
// 我们通常在业务中可多采用点状对象函数(规定参数对象类型)
const offDuty = (value: { x: number; y: string }) => {
console.log("x is ", value.x);
console.log("y is ", value.y);
}
// 业务中一定会涉及到"可选属性";先简单介绍下方便快捷的“可选属性”
const offDuty = (value: { x: number; y?: string }) => {
console.log("必选属性x ", value.x);
console.log("可选属性y ", value.y);
console.log("可选属性y的方法 ", value.y.toLocaleLowerCase());
}
offDuty({ x: 123, y: 'jiawen' })
offDuty({ x: 123 })
// 提问: 上述代码有问题吗?
答案:
// offDuty({ x: 123 }) 会导致结果报错value.y.toLocaleLowerCase()
// Cannot read property 'toLocaleLowerCase' of undefined
方案1: 手动类型检查
const offDuty = (value: { x: number; y?: string }) => {
if (value.y !== undefined) {
console.log("可能不存在的 ", value.y.toUpperCase());
}
}
方案2:使用可选属性 (推荐)
const offDuty = (value: { x: number; y?: string }) => {
console.log("可能不存在的 ", value.y?.toLocaleLowerCase());
}
unknown and any
// unknown 可以表示任意类型,但它同时也告诉TS, 开发者对类型也是无法确定,做任何操作时需要慎重
let Jiaven: unknown
Jiaven.toFixed(1) // Error
if (typeof Jiaven=== 'number') {
Jiaven.toFixed(1) // OK
}
当我们使用any类型的时候,any会逃离类型检查,并且any类型的变量可以执行任意操作,编译时不会报错
anyscript === javascript
注意:any 会增加了运行时出错的风险,不到万不得已不要使用;
如果遇到想要表示【不知道什么类型】的场景,推荐优先考虑 unknown
union type
union也叫联合类型,由两个或多个其他类型组成,表示可能为任何一个的值,类型之间用 ' | '隔开
type dayOff = string | number | boolean
联合类型的隐式推导可能会导致错误,遇到相关问题请参考语雀 code and tips —— 《TS的隐式推导》
.值得注意的是,如果访问不共有的属性的时候,会报错,访问共有属性时不会.上个最直观的demo
function dayOff (value: string | number): number {
return value.length;
}
// number并不具备length,会报错,解决方法:typeof value === 'string'
function dayOff (value: string | number): number {
return value.toString();
}
// number和string都具备toString(),不会报错
never
// never是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。
// 那never在实际开发中到底有什么作用? 这里笔者原汁原味照搬尤雨溪的经典解释来做第一个例子
第一个例子,当你有一个 union type:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
在 switch 当中判断 type,TS是可以收窄类型的 (discriminated union):
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break
case 'bar':
// val 在这里是 Bar
break
default:
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}
注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。
如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:
type All = Foo | Bar | Baz
然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,
这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。
所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。
第二个用法 返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
throw new Error(message);
}
第三个用法 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
while (true) {}
}
void
interface IProps {
onOK: () => void
}
void 和 undefined 功能高度类似,但void表示对函数的返回值并不在意或该方法并无返回值
enum
笔者认为ts中的enum是一个很有趣的枚举类型,它的底层就是number的实现
1.普通枚举
enum Color {
Red,
Green,
Blue
};
let c: Color = Color.Blue;
console.log(c); // 2
2.字符串枚举
enum Color {
Red = 'red',
Green = 'not red',
};
3.异构枚举 / 有时也叫混合枚举
enum Color {
Red = 'red',
Num = 2,
};
<第一个坑>
enum Color {
A, // 0
B, // 1
C = 20, // 20
D, // 21
E = 100, // 100
F, // 101
}
若初始化有部分赋值,那么后续成员的值为上一个成员的值加1
<第二个坑> 这个坑是第一个坑的延展,稍不仔细就会上当!
const getValue = () => {
return 23
}
enum List {
A = getValue(),
B = 24, // 此处必须要初始化值,不然编译不通过
C
}
console.log(List.A) // 23
console.log(List.B) // 24
console.log(List.C) // 25
如果某个属性的值是计算出来的,那么它后面一位的成员必须要初始化值。
否则将会 Enum member must have initializer.
Generic
author understands the generics in the vernacular: do not specify the specific type first, get the specific type
Let’s start with the following filter-demo and explore why a generic
Generic basic style
function fun<T>(args: T): T { return args }
Would you feel a bit embarrassed if you haven't touched it? It's ok! We go deep directly from the business point of view——
1.刚开始的需求:过滤数字类型的数组 declare function filter( array: number[], fn: (item: unknown) => boolean ) : number[]; 2.产品改了需求:还要过滤一些字符串 string[] 彳亍,那就利用函数的重载, 加一个声明, 虽然笨了点,但是很好理解 declare function filter( array: string[], fn: (item: unknown) => boolean ): string[]; declare function filter( array: number[], fn: (item: unknown) => boolean ): number[]; 3.产品又来了! 这次还要过滤 boolean[]、object[] .......... 这个时候如果还是选择重载,将会大大提升工作量,代码也会变得越来越累赘,这个时候泛型就出场了, 它从实现上来说更像是一种方法,通过你的传参来定义类型,改造如下: declare function filter<T>( array: T[], fn: (item: unknown) => boolean ): T[];
generic type can be anything, but most of the preferences are T, U, S, etc.
When we understand generics as a method to achieve, then we naturally think: methods have multiple parameters, default values, generics can also be
type Foo<T, U = string> = { // 多参数、默认值
foo: Array<T> // 可以传递
bar: U
}
type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number, number> // type B = { foo: number[]; bar: number; }
is a "function", there will be "restrictions". Here are some slightly common constraints
1. extends: 限制 T 必须至少是一个 XXX 的类型
type dayOff<T extends HTMLElement = HTMLElement> = {
where: T,
name: string
}
2. Readonly<T>: 构造一个所有属性为readonly,这意味着无法重新分配所构造类型的属性。
interface Eat {
food: string;
}
const todo: Readonly<Eat> = {
food: "meat beef milk",
};
todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.
3. Pick<T,K>: 从T中挑选出一些K属性
interface Todo {
name: string;
job: string;
work: boolean;
type TodoPreview = Pick<Todo, "name" | "work">;
const todo: TodoPreview = {
name: "jiawen",
work: true,
};
todo;
4. Omit<T, K>: 结合了 T 和 K 并忽略对象类型中 K 来构造类型。
interface Todo {
name: string;
job: string;
work: boolean;
}
type TodoPreview = Omit<Todo, "work">;
const todo: TodoPreview = {
name: "jiawen",
job: 'job',
};
5.Record: 约束 定义键类型为 Keys、值类型为 Values 的对象类型。
enum Num {
A = 10001,
B = 10002,
C = 10003
}
const NumMap: Record<Num, string> = {
[Num.A]: 'this is A',
[Num.B]: 'this is B'
}
// 类型 "{ 10001: string; 10002: string; }" 中缺少属性 "10003",
// 但类型 "Record<ErrorCodes, string>" 中需要该属性,所以我们还可以通过Record来做全面性检查
keyof 关键字可以用来获取一个对象类型的所有 key 类型
type User = {
id: string;
name: string;
};
type UserKeys = keyof User; // "id" | "name"
改造如下
type Record<K extends keyof any, T> = {
[P in K]: T;
};
此时的 T 为 any;
还有一些不常用,但是很易懂的:
6. Extract<T, U> 从T,U中提取相同的类型
7. Partial<T> 所有属性可选
type User = {
id?: string,
gender: 'male' | 'female'
}
type PartialUser = Partial<User> // { id?: string, gender?: 'male' | 'female'}
type Partial<T> = { [U in keyof T]?: T[U] }
8. Required<T> 所有属性必须 << === >> 与Partial相反
type User = {
id?: string,
sex: 'male' | 'female'
}
type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}
function showUserProfile (user: RequiredUser) {
console.log(user.id) // 这时候就不需要再加?了
console.log(user.sex)
}
type Required<T> = { [U in keyof T]-?: T[U] }; -? : 代表去掉?
Three, some tips for TS
TS type and interface
interface (interface) can only declare object types, support declaration merging (extensible).
interface User { id: string } interface User { name: string } const user = {} as User console.log(user.id); console.log(user.name);
- type (type alias) does not support declaration merging - l type
type User = {
id: string,
}
if (true) {
type User = {
name: string,
}
const user = {} as User;
console.log(user.name);
console.log(user.id) // 类型“User”上不存在属性“id”。
}
🌟🌟🌟🌟🌟🌟
Type and interface summary of similarities and differences:
- Generally speaking, type is more general, and the right side can be any type, including expression operations, mapping, etc.;
- can be defined by interface, and type can also be used;
- has different extension methods. Interface can be extended with the extends keyword, or used to implement an interface;
- can be used to describe an object or function;
- type can declare basic type aliases, union types, and tuple types, but interface cannot;
- ⚠️ But if you are developing a package or module and allow others to extend it, use interface. If you need to define basic data types or need type operations, use type.
- interface can be defined multiple times and will be treated as a combined declaration, and type is not supported;
- export method is different, interface supports simultaneous declaration and export by default, and typetype must be declared first and then exported;
TS script mode and module mode
Typescript has two modes. The logic to distinguish is that the file content package does not contain import or export keywords
script mode (Script) A file corresponds to a script tag of html,
Module mode (Module) A file corresponds to a Typescript module.script mode, all variable definitions and type declarations are global. Multiple file definitions for the same variable will report an error, and the interface with the same name will be merged. In module mode, all variable definitions and type declarations are valid within the module.
There are also differences in writing type declarations between the two modes. For example, in script mode, you can directly declare var GlobalStore to write declarations for global objects.
example:
Directly declare var GlobalStore in script mode to write declarations for global objects.
GlobalStore.foo = "foo"; GlobalStore.bar = "bar"; // Error declare var GlobalStore: { foo: string; };
In module mode, declare global is required to write declarations for global objects
GlobalStore.foo = "foo"; GlobalStore.bar = "bar"; declare global { var GlobalStore: { foo: string; bar: string; }; } export {}; // export 关键字改变文件的模式
TS index signature
Index signatures can be used to define the type of properties and values in the object, for example, to define a React component, allowing Props to pass any props whose key is string and value is number
interface Props { [key: string]: number } <Component count={1} /> // OK <Component count={true} /> // Error <Component count={'1'} /> // Error
Type in TS
Typescript allows you to use types like objects take attribute values
type User = { userId: string friendList: { fristName: string lastName: string }[] } type UserIdType = User['userId'] // string type FriendList = User['friendList'] // { fristName: string; lastName: string; }[] type Friend = FriendList[number] // { fristName: string; lastName: string; }
In the above example, we have calculated several other types from the User type by using the type keying function. FriendList[number] where number is a keyword, used to get the type of the array item. You can also use literal numbers in tuples to get the type of array elements.
type group = [number, string] type First = group[0] // number type Second = group[1] // string
Assertion of TS
- type assertion is not a type conversion, asserting that a type that does not exist in a union type is not allowed
function getLength(value: string | number): number {
if (value.length) {
return value.length;
} else {
return value.toString().length;
}
// 这个问题在object of parmas已经提及,不再赘述
修改后:
if ((<string>value).length) {
return (<string>value).length;
} else {
return something.toString().length;
}
}
断言的两种写法
1. <类型>值: <string>value
2. 或者 value as string
特别注意!!! 断言成一个联合类型中不存在的类型是不允许的
function toBoolean(something: string | number): boolean {
return <boolean>something;
}
- Non-empty predicate!
TypeScript also has a special syntax for removing null and undefined from types without any explicit checks. ! Writing after any expression is actually a type assertion, indicating that the value is not null or undefined
function liveDangerously(x?: number | undefined | null) {
// 推荐写法
console.log(x!.toFixed());
}
Fourth, how to use TS in Hook component
usestate
If the initial value of useState is not null/undefined, it is capable of type inference, and the type is inferred based on the initial value passed in; if the initial value is null/undefined, the type definition needs to be passed to be constrained. In general, it is recommended to pass in the type (through the first generic parameter of useState).
// 这里ts可以推断 value的类型并且能对setValue函数调用进行约束 const [value, setValue] = useState(0); interface MyObject { name: string; age?: number; } // 这里需要传递MyObject才能约束 value, setValue // 所以我们一般情况下推荐传入类型 const [value, setValue] = useState<MyObject>(null);
-----as unkonwn as unkownun
useEffect useLayoutEffect
- No return value, no type transfer and constraints
useMemo useCallback
- useMemo does not need to pass the type, it can infer the type based on the return value of the function.
- useCallback does not need to pass the type, the type can be inferred based on the return value of the function.
But note that the input parameters of the function need to define the type, otherwise it will be inferred as any!
const value = 10;
const result = useMemo(() => value * 2, [value]); // 推断出result是number类型
const multiplier = 2;
// 推断出 (value: number) => number
// 注意函数入参value需要定义类型
const multiply = useCallback((value: number) => value * multiplier, [multiplier]);
useRef
The type can be inferred when useRef passes a non-empty initial value, and the type can also be defined by passing in the first generic parameter to constrain the type of ref.current.
- If the passed value is null
const MyInput = () => {
const inputRef = useRef<HTMLInputElement>(null); // Here the constraint inputRef is an html element
return <input ref={inputRef} />
} If not null
const myNumberRef = useRef(0); // It is automatically inferred that myNumberRef.current is of type number
myNumberRef.current += 1;### useContext
UseContext generally can infer the return value based on the value of the passed in Context. Generally, there is no need to display the delivery type
type Theme = 'light' | 'dark'; // 我们在createContext就传了类型了 const ThemeContext = createContext<Theme>('dark'); const App = () => ( <ThemeContext.Provider value="dark"> <MyComponent /> </ThemeContext.Provider> ) const MyComponent = () => { // useContext根据ThemeContext推断出类型,这里不需要显示传 const theme = useContext(ThemeContext); return <div>The theme is {theme}</div>;
Five, some thoughts about TS
1. About how TSC converts TS code to JS code
这个部分比较冗长,后续可以单独出一篇文章(2)来专门探索。
However, some of the commonly used configuration attribute tables of tsconfig.json are still worth mentioning
{ "compilerOptions": { "noEmit": true, // 不输出文件 "allowUnreachableCode": true, // 不报告执行不到的代码错误。 "allowUnusedLabels": false, // 不报告未使用的标签错误 "alwaysStrict": false, // 以严格模式解析并为每个源文件生成 "use strict"语句 "baseUrl": ".", // 工作根目录 "lib": [ // 编译过程中需要引入的库文件的列表 "es5", "es2015", "es2016", "es2017", "es2018", "dom" ] "experimentalDecorators": true, // 启用实验性的ES装饰器 "jsx": "react", // 在 .tsx文件里支持JSX "sourceMap": true, // 是否生成map文件 "module": "commonjs", // 指定生成哪个模块系统代码 "noImplicitAny": false, // 是否默认禁用 any "removeComments": true, // 是否移除注释 "types": [ //指定引入的类型声明文件,默认是自动引入所有声明文件,一旦指定该选项,则会禁用自动引入,改为只引入指定的类型声明文件,如果指定空数组[]则不引用任何文件 "node", // 引入 node 的类型声明 ], "paths": { // 指定模块的路径,和baseUrl有关联,和webpack中resolve.alias配置一样 "src": [ //指定后可以在文件之直接 import * from 'src'; "./src" ], }, "target": "ESNext", // 编译的目标是什么版本的 "outDir": "./dist", // 输出目录 "declaration": true, // 是否自动创建类型声明文件 "declarationDir": "./lib", // 类型声明文件的输出目录 "allowJs": true, // 允许编译javascript文件。 }, // 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件) "include": [ "src/**/*" ], // 指定一个排除列表(include的反向操作) "exclude": [ "demo.ts" ], // 指定哪些文件使用该配置(属于手动一个个指定文件) "files": [ "demo.ts" ] }
2. The underlying implementation of TS generics
关于TS泛型进阶篇 链接:[https://dtstack.yuque.com/rd-center/sm6war/wae3kg](https://dtstack.yuque.com/rd-center/sm6war/wae3kg)
这个部分比较复杂,笔者还需沉淀,欢迎各位直接留言或在文章中补充!!!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。