关于javascript:TS基础应用-Hook中的TS

6次阅读

共计 12880 个字符,预计需要花费 33 分钟才能阅读完成。

说在后面

   本文难度偏中下,波及到的点大多为如何在我的项目中正当利用 ts,小局部会波及一些原理,受众面较广,有无 TS 根底均可释怀食用。**>>>> 阅完本文,您可能会播种到 <<<<**
  1. 若您还不相熟 TS,那本文可帮忙您实现 TS 利用局部的学习,随同泛滥 Demo 例来疏导业务利用;
  2. 若您比拟相熟 TS,那本文可当作温习文,带您回顾常识,心愿能在某些点引发您新发现和思考;
  3. 针对于 class 组件的 IState 和 IProps,类比 Hook 组件的局部写法和思考;

🌟🌟🌟TIPS:超好用的在线 TS 编辑器(诸多配置项可手动配置)传送门:TS 在线 🌟🌟🌟

一、什么是 TS

不扯艰涩的概念,艰深来说 TypeScript 就是 JavaScript 的超集,它具备可选的类型,并能够编译为纯 JavaScript 运行。(笔者始终就把 TypeScript 看作 JavaScript 的 Lint)那么问题来了,为什么 TS 肯定要设计成动态的?或者换句话说,咱们为什么须要向 JavaScript 增加类型标准呢?

经典自问自答环节——因为它能够解决一些 JS 尚未解决的痛点:

  1. JS 是动静类型的语言,这也意味着在实例化之前咱们都不晓得变量的类型,然而应用 TS 能够在运行前就防止经典低级谬误。例:Uncaught TypeError:’xxx’ is not a function

⚠️ 典中典级别的谬误🌰:

JS 就是这样,只有在运行时产生了谬误才通知我有错,然而当 TS 染指后:


好家伙!间接把问题在编辑器阶段抛出,nice!

  1. 懒人狂欢。标准不便,又不容易出错,对于 VS Code,它能做的最多只是标示出有没有这个属性,但并不能准确的表明这个属性是什么类型,但 TS 能够通过类型推导 / 反推导(说文言:如果您未明确编写类型,则将应用类型推断来推断您正在应用的类型),从而完满优化了代码补全这一项:

第一个 Q&A——思考:

那么咱们还能想到在业务开发中 TS 解决了哪些 JS 的痛点呢?(发问)

答复,总结,补充:
- 对函数参数的类型限度;
- 对数组和对象的类型限度,防止定义出错 例如数据解构简单或较多时,
可能会呈现数组定义谬误 a = {}, if (a.length){// xxxxx}
-let functionA = ‘jiawen’ // 实际上 let functionA: string = ‘jiawen’​

  1. 使咱们的利用代码更易浏览和保护,如果定义欠缺,能够通过类型大抵明确参数的作用;

置信通过上述简略的 bug-demo,各位已对 TS 有了一个初步的重新认识
接下来的章节便正式介绍咱们在业务开发过程中如何用好 TS

二、怎么用 TS

 在业务中如何用 TS/ 如何用好 TS?这个问题其实和 "在业务中怎么用好一个 API" 是一样的。首先要晓得这个货色在干嘛,参数是什么,规定是什么,可能承受有哪些扩大...... 等等。简而言之,撸它!

TS 罕用类型演绎

通过对业务中常见的 TS 谬误做出的一个综合性总结演绎,心愿 Demos 会对您有播种

元语 (primitives) 之 string number boolean

  笔者把根本类型拆开的起因是: 不论是中文还是英文文档,primitives/ 元语 / 元组 这几个名词都频繁出镜,笔者了解的文言:心愿在类型束缚定义时,应用的是字面量而不是内置对象类型,官网文档:

let a: string = 'jiawen';
let flag: boolean = false;
let num: number = 150

interface IState: {
  flag: boolean;
  name: string;
  num: number;
}

元组

// 元组类型示意已知元素数量和类型的数组,各元素的类型不用雷同,然而对应地位的类型须要雷同。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 和 {}

// 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 与 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 联结类型

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.

泛型

笔者了解的泛型很文言:先不指定具体类型,通过传入的参数类型来失去具体类型
咱们从下述的 filter-demo 动手,摸索一下为什么肯定须要泛型

  • 泛型的根底款式

    function fun<T>(args: T): T {return args}

    如果没接触过,是不是会感觉有点懵?没关系!咱们间接从业务角度深刻——

    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[];
    

    泛型中的 <T> 能够是任意,然而大部分偏好为 T、U、S 等,

当咱们把泛型了解为一种办法实现后,那么咱们便很天然的联想到:办法有多个参数、默认值,泛型也能够

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; }

既然是“函数”,那也会有“限度”,下文列举一些略微常见的束缚

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] };   -? : 代表去掉?

三、TS 的一些须知

TS 的 type 和 interface

  • interface(接口)只能申明对象类型,反对申明合并(可扩大)。

    interface User {id: string}
     
    interface User {name: string}
     
    const user = {} as User
     
    console.log(user.id);
    console.log(user.name);
    
  • type(类型别名)不反对申明合并 — l 类型
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 和 interface 异同点总结:

  1. 通常来讲 type 更为通用,右侧能够是任意类型,包含表达式运算,以及映射等;
  2. 但凡可用 interface 来定义的,type 也可;
  3. 扩大形式也不同,interface 能够用 extends 关键字进行扩大,或用来 implements 实现某个接口;
  4. 都能够用来形容一个对象或者函数;
  5. type 能够申明根本类型别名、联结类型、元组类型,interface 不行;
  6. ⚠️ 但如果你是在开发一个包,模块,容许他人进行扩大就用 interface,如果须要定义根底数据类型或者须要类型运算,应用 type。
  7. interface 能够被屡次定义,并会被视作合并申明,而 type 不反对;
  8. 导出形式不同,interface 反对同时申明并默认导出,而 typetype 必须先申明后导出;

TS 的脚本模式和模块模式

Typescript 存在两种模式,辨别的逻辑是,文件内容包不蕴含 import 或者 export 关键字

脚本模式(Script)一个文件对应一个 html 的 script 标签,
模块模式(Module)一个文件对应一个 Typescript 的模块。

脚本模式下,所有变量定义,类型申明都是全局的,多个文件定义同一个变量会报错,同名 interface 会进行合并;而模块模式下,所有变量定义,类型申明都是模块内无效的。

两种模式在编写类型申明时也有区别,例如脚本模式下间接 declare var GlobalStore 即可为全局对象编写申明。

例子:

  • 脚本模式下间接 declare var GlobalStore 即可为全局对象编写申明。

    GlobalStore.foo = "foo";
    GlobalStore.bar = "bar"; // Error
    
    declare var GlobalStore: {foo: string;};
  • 模块模式下,要为全局对象编写申明须要 declare global

    GlobalStore.foo = "foo";
    GlobalStore.bar = "bar";
    
    declare global {
    var GlobalStore: {
      foo: string;
      bar: string;
    };
    }
    
    export {}; // export 关键字扭转文件的模式
    

    TS 的索引签名

  • 索引签名能够用来定义对象内的属性、值的类型,例如定义一个 React 组件,容许 Props 能够传任意 key 为 string,value 为 number 的 props

    interface Props {[key: string]: number
    }
    
    <Component count={1} /> // OK
    <Component count={true} /> // Error
    <Component count={'1'} /> // Error

    TS 的类型键入

  • Typescript 容许像对象取属性值一样应用类型

    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;}
  • 在下面的例子中,咱们利用类型键入的性能从 User 类型中计算出了其余的几种类型。FriendList[number]这里的 number 是关键字,用来取数组子项的类型。在元组中也能够应用字面量数字失去数组元素的类型。

    type group = [number, string]
    type First =  group[0] // number
    type Second = group[1] // string

    TS 的断言

  • 类型断言不是类型转换,断言成一个联结类型中不存在的类型是不容许的
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;}
  • 非空断言符!

TypeScript 还具备一种非凡的语法,用于从类型中删除 null 和 undefined 不进行任何显式查看。! 在任何表达式之后写入实际上是一个类型断言,表明该值不是 null 或 undefined

function liveDangerously(x?: number | undefined | null) {
  // 举荐写法
  console.log(x!.toFixed());
}

四、如何在 Hook 组件中应用 TS

usestate

  • useState 如果初始值不是 null/undefined 的话,是具备类型推导能力的,依据传入的初始值推断出类型;初始值是 null/undefined 的话则须要传递类型定义能力进行束缚。个别状况下,还是举荐传入类型(通过 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

  • 没有返回值,无需类型传递和束缚

useMemo useCallback

  • useMemo 无需传递类型, 依据函数的返回值就能推断出类型。
  • useCallback 无需传递类型,依据函数的返回值就能推断出类型。

然而留神函数的入参须要定义类型,不然将会推断为 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

  • useRef 传非空初始值的时候能够推断类型,同样也能够通过传入第一个泛型参数来定义类型,束缚 ref.current 的类型。

  • 如果传值为 null
    const MyInput = () => {
    const inputRef = useRef<HTMLInputElement>(null); // 这里束缚 inputRef 是一个 html 元素
    return <input ref={inputRef} />
    }
  • 如果不为 null
    const myNumberRef = useRef(0); // 主动推断出 myNumberRef.current 是 number 类型
    myNumberRef.current += 1;

    ### useContext
  • useContext 个别依据传入的 Context 的值就能够推断出返回值。个别无需显示传递类型

    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>;

    五、对于 TS 的一些思考

1. 对于 TSC 如何把 TS 代码转换为 JS 代码

这个局部比拟简短,后续能够独自出一篇文章(2)来专门摸索。
  • 不过,tsconfig.json 的局部罕用的配置属性表还是值得一提的

    {
    "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. TS 泛型的底层实现

对于 TS 泛型进阶篇 链接:[https://dtstack.yuque.com/rd-center/sm6war/wae3kg](https://dtstack.yuque.com/rd-center/sm6war/wae3kg)

这个局部比较复杂,笔者还需积淀,欢送各位间接留言或在文章中补充!!!

3. TS 泛型 + 类型反推在理论开发中的利用

正文完
 0