我不想开学之TS

TS简介

TS是JS的静态类型检查器,也成为静态编译;添加了可选的静态类型、和基于类的面向对象编程,不会影响程序的基本运行

JS、ES、TS的关系

ES是JS的标准,TS是JS的超集

TS的竞品

  • TypeScript:核心是静态类型系统,提供编译时类型检查

  • ESLint/TSLint:专注于代码风格和质量检查(如命名规范、潜在错误),不直接提供类型系统。其中ESLint支持 JS 和 TS,架构更适合增量检查

  •  CoffeeScript:改进 JS 语法结果被JS反超

  • Flow:Facebook 的静态类型检查器,类似 TS,但更强调与现有 JS 代码的兼容;但维护力度下降

ts的特点

静态类型代码:预测代码

安装ts=>npm install typescript -g =>运行代码=>tsc 文件名.ts=>生成对应js文件,运行该文件=>node 文件名.js

类型别名:类型名字=类型

因为ts生成js文件后会产生两个相同的函数,所以命名重复,并且每次修正ts都要重新tsc该文件

所以我们进行了优化编译:

优化编译

解决ts和js的命名冲突问题:tsc --init,生产配置文件

自动编译:tsc --watch

发出错误:tsc -noEmitOnError hello.ts

显式类型:形如function 函数名(参数名:参数类型。参数名:参数类型)

function greet(person, data) {
    console.log("hello ".concat(person, " ").concat(data));
}
greet('august',new Date());

ts会根据类型初始定义推断变量的类型:

降级编译

不适用es6语法的文件可以通过修改tsconfig.json的target属性来降级

严格模式

    "strict": true,//注释掉该属性,取消严格模式

等效开启这些设置项:

{
  "compilerOptions": {
    // 禁止隐式any类型(必须显式声明any类型)
    "noImplicitAny": true,

    // 严格的null/undefined检查(必须明确处理null和undefined)
    "strictNullChecks": true,

    // 严格的函数类型检查(函数参数类型必须严格匹配) 
    "strictFunctionTypes": true,

    // 严格的bind/call/apply检查(确保调用方法时参数类型正确)
    "strictBindCallApply": true,

    // 严格的类属性初始化检查(类属性必须初始化)
    "strictPropertyInitialization": true,

    // 禁止隐式this类型(必须明确this的类型)
    "noImplicitThis": true,

    // 生成的JS代码中使用严格模式(自动添加"use strict")
    "alwaysStrict": true
  }
}

数据类型

基元类型

string、number、boolean

//ts
let str: string = 'hello world'
let number:number=100
let bool:boolean=true

经过tsc --watch以后生成对应的js

"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
let str = 'hello world';
let number = 100;
let bool = true;
//# sourceMappingURL=01-hello-world.js.map

数组类型

数组的定义:type[]和Array<type>,type为任意合法的类型

let arr: number[] = [1, 2, 3, 4]
let arr2: Array<number> = [1, 1, 1, ]

any类型

当你不希望某个特定值导致类型检查错误,可以使用any:

let obj: any = {
  x: 0,
}
obj.foo()
obj()
obj.bar = 100
obj = 'hello'
const n:number=obj

这么写ts是不会报错的,但生成的js无法运行

函数类型

在ts里使用函数,运行我们指定函数的输入和输出值的类型

function greet(name: string) {
    console.log('Hello,'+name.toUpperCase()+'!!')
}
greet('august')
//Hello,AUGUST!!
function getFavoriteNumber(num:number):number {
   return num
}
//100
console.log(getFavoriteNumber(100))
const names = ['august', 'hello', 'world']
names.forEach(name=>{
    console.log(name.toUpperCase())
})

//AUGUST
//HELLO
//WORLD

对象类型

function printCoord(pt:{x:number, y:number}){
    console.log('x坐标为:'+pt.x, 'y坐标为:'+pt.y)
}
printCoord({x:100, y:200})

function printName(obj:{first:string, last?:string}){//last为可选
//    if(obj.last!==undefined){
//     console.log(obj.first+obj.last)
//    }
console.log(obj.last?.toUpperCase())//如果last存在,则转换为大写
}
//printName({ first: 'august', last: 'hello', xxx: 'aaaa' })不能额外传递属性
printName({ first: 'august', last: 'hello' })

联合类型

允许我们从现有类型中构建新类型,表示可能是这些类型中的一个

function printId(id: number | string | number[]) {
  //console.log(id.toUpperCase())类型“string | number”上不存在属性“toUpperCase”。
  //类型“number”上不存在属性“toUpperCase”。
  if (typeof id === 'string') {
    console.log(id.toUpperCase())
  } else {
    console.log(id)
  }
}
printId(101)//101
printId('aaa')//AAA
// printId({
//     id:101
// })类型“{ id: number; }”的参数不能赋给类型“string | number”的参数。

function welcomePeople(x: string[] | string) {
  if (Array.isArray(x)) {
    console.log('hello' + x.join(' and '))
  } else {
    console.log('hello' + x)
  }
}
welcomePeople(['august', 'hello world'])//helloaugust and hello world
function getFirstThree(x: number[] | string) {
  return x.slice(0, 3)
}
const arr = getFirstThree([1, 2, 3, 4, 5])
console.log(arr)//[ 1, 2, 3 ]

为了方便,我们可以给重新组合起来的元素起别名来多次使用

类型别名

可以定义对象类型

type Point = {
  x: number
  y: number
}
function printCoord(pt: Point) {
  console.log("The coordinate's x value is " + pt.x)
  console.log("The coordinate's y value is " + pt.y)
}
printCoord({
  x: 100,
  y: 100,
})//The coordinate's x value is 100
//The coordinate's y value is 100


type ID = number | string
function printId(id: ID) {
  console.log(id)
}
printId(101)//101
printId('aaa')//aaa

//type定义变量类型
type UserInputSanitized = string | number
function sanitizeInput(str: string):UserInputSanitized {
  return str.trim()
}
let userInput = '  Hello, world!  '
let sanitized = sanitizeInput(userInput)
console.log(sanitized)//Hello, world!

接口类型

是一种定义对象的方式,通过关键字interface来定义

interface Point{
    x: number
    y: number
}
function printCoord(pt:Point) {
    console.log(`The coordinate's x value is ${pt.x}`)
    console.log(`The coordinate's y value is ${pt.y}`)
}
printCoord({
    x: 100,
    y: 100,
})

几乎所有interface定义的类型都可以通过type类型别名来定义

interface Animal{
    name: string
}
interface Bear extends Animal{
    honey: boolean
}//继承
const bear:Bear={
    name: 'bear',
    honey: true,
}
console.log(bear.name)//bear
console.log(bear.honey)//true

如上的代码等效于如下代码:

type Animal = {
    name:string
}
//使用&号合并类型
type Bear = Animal & {
    honey: boolean
}
const bear:Bear={
    name: 'bear',
    honey: true,
}

但是type不能合并同字段的属性,interface可以

//向现有的类型添加字段
interface MyWindow{
    count:number
}
interface MyWindow{
    title:string

} 
const w: MyWindow = {
    count: 100,
    title: 'hello world',
}//两次同名的接口合并属性成功

type不能向已有字段添加,只能用前面说过的&继承

//类型创建以后不能更改
type MyWindow = {
   title:string
}
type Mywindow = {
    count: number
}
//只能通过&符号合并
type MyWindowAndMywindow = MyWindow & Mywindow
const w: MyWindowAndMywindow = {
    title: 'hello world',
    count: 100,
}
console.log(w.count)//100

类型断言

断言指的是开发者手动指定值的类型的方式,可以理解为告诉编译器“我知道这个值的具体类型,不需要你检查”,但是不会真的转换数据类型,不过你要承担断言错误的风险

两种定义方式

与类型注释相同,不影响代码的运行时行为,在编译后被删除

const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement

const myCanvas2 = <HTMLCanvasElement>document.getElementById('main_canvas')
//告诉编译器document.getElementById('main_canvas')返回的元素是HTMLCanvasElement 类型(而不是默认的 HTMLElement)

ts可以防止不可能的强制的发生

断言本身是一种强制告诉编译器我作为开发者已知该类型,“防止不可能的强制的发生”的意思是:当你想把一个值断言成一个它 根本不可能属于的类型 时,TypeScript 会报错,防止这种不安全的操作。

const x = ('hello' as any) as number//双重类型断言,强制将hello断言为一个number

也可以写为unknown,在语义上更明确

const x = ('hello' as unknown) as number//双重类型断言,强制将hello断言为一个number

文字类型

文字类型表示一个固定的值,类似于let和const的区别

例如下面这种情况,const是不能变的

let testString = 'hello'
testString = 'world'

const testString2 = 'hello'
//testString2 = 'world'//error

等效于我给x定为类型是“hello”的字符串,那么它就不能是别的类型,x必须是字符串且为“hello”

let x: 'hello' = 'hello'
//x='world'//不能将类型“"world"”分配给类型“"hello"”。

文字类型是死板的,例如这里的aligement必须是left、right、center

function printText(s: string, aligement: 'left' | 'right' | 'center') {
  console.log(s + " " + aligement)
}
printText('hello', 'left')//hello left
printText('hello', 'right') //hello right
printText('hello', 'center')//hello center

应用举例:

比大小返回对应的结果

function compare(a: string, b: string): -1 | 0 | 1 {
  return a === b ? 0 : a > b ? 1 : -1
}

进行类型收窄:

interface Options {
  width: number
}
function config(x: Options | 'auto') { console.log('Auto mode')
}
config({ width: 100 })
config('auto')

布尔文字类型:

let b1: true = true
let b2: false = false

文字推理

里面也提到了断言的用法,限制类型放宽

文字推理决定了 TypeScript 是将一个值视为具体的字面量类型,还是更宽泛的原始类型。在这里我们限制req.method必须是‘GET’字面量而不是字符串,这就是文字推理

function handleRequest(url: string, method: 'GET' | 'POST') {
  console.log(url, method)
}
const req = {
  url: 'https://www.bilibili.com',
  method: 'GET' as const, //告诉 TypeScript 不要放宽类型推断,保持 'GET' 的字面量类型
}
handleRequest(req.url,req.method)


//另一种写法


function handleRequest(url: string, method: 'GET' | 'POST') {
  console.log(url, method)
}
const req = {
  url: 'https://www.bilibili.com',
  method: 'GET'
}
handleRequest(req.url, req.method as 'GET')

null和undefined类型

和联合类型一起用

let x = undefined //自动推断
let y = null //自动推断
let x // 类型推断为 any,值为 undefined
//let z: string=undefined 严格模式下错误

function doSomething(x: string | null) {
  if (x === null) {
    console.log('x is null')
  } else {
    console.log(x.toUpperCase())
  }
}
function liveDangerously(x?:(number | null)) {
    console.log(x!.toFixed(2))
//“!”断言 x 不是 null/undefined,强制允许调用 toFixed
}
liveDangerously(100)//100.00
liveDangerously(null)//运行时错误
liveDangerously(undefined)//运行时错误

枚举

确定需要枚举的时候使用,否则也没必要使用

enum Direction {
    Up=1,
    Down,
    Left,
    Right,
}
console.log(Direction.Up)//1
console.log(Direction.Down)//2
console.log(Direction.Left)//3
console.log(Direction.Right)//4

不常用的原语

bigint:非常大的整数

symbol:全局唯一的引用

const oneHundred: bigint = BigInt(100)
const anotherHundred: bigint = 100n

const firstSymbol = Symbol('name')
const secondSymbol = Symbol('name')
//console.log(firstSymbol === secondSymbol) 始终返回false

类型缩小

宽类型->窄类型,一般用在联合类型里

前面我们用断言处理过,也可以加个条件判断

详细介绍类型缩小

类型守卫

收窄变量的类型范围

例如这样:

function printAll(str: string | string[] | null) {
    if (typeof str === 'object') {//是数组就遍历
        for (const s of str) {
          console.log(s) //“str”可能为 “null”,因为null也是object
        }       
    }else if(typeof str==='string'){//是字符串就打印
        console.log(str)
    } else {
        //...
    }

}

但是上面的null也是object,这样判断是有问题的,后面会讲

真值缩小

使用条件、&&、||、if语句、布尔否定(!)

使用&&修改前面的代码

function printAll(str: string | string[] | null) {
  if (str && typeof str === 'object') {
    //是数组就遍历
    for (const s of str) {
      console.log(s) //&&进行非空检查,确保str不为null,再进行typeof判断

    }
  } else if (typeof str === 'string') {
    //是字符串就打印
    console.log(str)
  } else {
    //...
  }
}

示例:

function multiplyAll(
    values: number[] | undefined,
    factor:number
) {
    if(values){
        return values.map(x=>x*factor)
    }
    return []
}
console.log(multiplyAll([1, 2, 3], 2))//[ 2, 4, 6 ]
console.log(multiplyAll(undefined, 2))//[]

等值缩小

使用===、!==、==、!=

function example(x: string | number, y: string | boolean) {
  if (x === y) {
    //检查值和类型
    x.toUpperCase()
    y.toLowerCase()
  } else {
    console.log(x)
    console.log(y)
  }
}

还是修改前面的printAll函数:

function printAll(str: string | string[] | null) {
  if (str !== null) {
    if (typeof str === 'object') {
      //是数组就遍历
      for (const s of str) {
        console.log(s) //“str”可能为 “null”,因为null也是object
      }
    } else if (typeof str === 'string') {
      //是字符串就打印
      console.log(str)
    } else {
      //...
    }
  }
}

同时屏蔽null和undefined

interface Container{
    value:number|null|undefined
}
function multiplyValue(container: Container, factor: number) {
    if(container.value != null ){//!null是过滤undefined和null的常用写法
        console.log((container.value *= factor))
        
    }
}
multiplyValue({value:10},2)//10
multiplyValue({ value: undefined }, 6)//未进入分支
multiplyValue({ value: null }, 6)//未进入分支

in操作符缩小

因为有了联合类型,联合类型实际是由多个基础类型组成的,所以可以通过in操作符看组合类型是否含有某个基本类型

举例

type Fish = {
  swim: () => void
}
type Bird = {
  fly: () => void
}
function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    console.log('这是🐟')
  } else if ('fly' in animal) {
    console.log('这是鸟')
  } else {
    console.log('这是未知动物')
  }
}
const fish:Fish={
    swim:()=>{}
}
move(fish)

如果我们再添加一个人类组合类型,人类的游泳类型是可选的,这在判断的时候会出错,所以我们在这里使用了断言,当然断言并不是最好的解决办法

type Fish = {
  swim: () => void
}
type Bird = {
  fly: () => void
}
type Human = {
  swim?: () => void //人类可能会游泳
  walk: () => void
}

function move(animal: Fish | Bird | Human) {
  if ('swim' in animal) {
   return (animal as Fish).swim()
  } else if ('fly' in animal) {
    return (animal as Bird).fly()
  } else {
    console.log('这是未知动物')
  }
}
const XiaoMing: Human = {
    swim: () => { },
    walk: () => { },

}
move(XiaoMing)

instanceof操作符缩小

检查一个值是否为另一个值的实例

判断x是否为Date对象

function logValue(x:Date|string) {
    if (x instanceof Date) {
        console.log(x.toUTCString())//日期对象转换为 UTC 时间字符串
    } else {
        console.log(x.toUpperCase())//字符串对象转换为大写字符串
    }
}
logValue(new Date())//Tue, 28 Nov 2023 03:00:00 GMT
logValue('hello')//HELLO

分配缩小

以这个代码为例子可以看出x会根据我们一开始给遍历赋的值,ts会自动根据所赋的值来收窄该遍历的类型范围

我们限制了数字和字符串,就无法再赋值布尔值

//x为联合类型,包括number和string
let x = Math.random() < 0.5 ? 10 : 'hello'

x = 1
console.log(x)//1
x = `xixixi`
console.log(x)//xixixi
x=true//不能将类型“boolean”分配给类型“string | number”。

控制流分析

通过分析代码的执行路径(条件分支、循环、返回语句等)来 自动推断和收窄变量的类型,也是一种静态分析,在编译时进行,不影响运行

可以看出控制流一直在控制x的类型

function example() {
  let x: string | number | boolean
  x = Math.random() < 0.5
  //x:=boolean
  console.log(x)
  if (Math.random() < 0.5) {
    x = 'hello'
    //x:=string
    console.log(x)
  } else {
    x = 100
    //x:=number
    console.log(x)
  }
  return x
}
let x = example()
初始: string | number | boolean
    ↓
赋值: boolean (50% true, 50% false)
    ↓
条件分支:
    ├─ 50%: 赋值为 string ("hello")
    └─ 50%: 赋值为 number (100)
    ↓
最终: string | number

使用类型谓词

在这个函数里,如果pet是Fish,就返回true

其中用到了断言,就是我们假设pet一定有swim这个方法,只不过可能为undefined

一个类型守卫和类型谓词结合的例子

function getSmallPet(): Fish | Bird {
  let fish: Fish = {
    name: 'sharkey',
    swim: () => {},
  }
  let bird: Bird = {
    name: 'sparrow',
    fly: () => {},
  }
  return true ? bird : fish //返回一个Bird的宠物
}
let pet = getSmallPet()
if (isFish(pet)) {
  pet.swim()
} else {
  console.log(pet.name) //sparrow
  pet.fly()
}

这里的fliter用法的优势在于ts的过滤可以通过isFish里的类型谓词,自动过滤后的确切类型为Fish[]

function getSmallPet(): Fish | Bird {
  let fish: Fish = {
    name: 'sharkey',
    swim: () => {},
  }
  let bird: Bird = {
    name: 'sparrow',
    fly: () => {},
  }
  return true ? bird : fish //返回一个Bird的宠物
}
let pet = getSmallPet()
if (isFish(pet)) {
  pet.swim()
} else {
  console.log(pet.name) //sparrow
  pet.fly()
}
let zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()]
const underWater1: Fish[] = zoo.filter(isFish)//使用类型谓词过滤数组
const underWater2: Fish[] = zoo.filter(isFish) as Fish[]//使用类型断言,可能会冗余
const underWater3: Fish[] = zoo.filter((pet): pet is Fish => {
  if (pet.name === 'frog') {
    return false
  }
  return isFish(pet)
})//灵活的过滤方式,自定义类型谓词
//三个都返回空数组

unions

举个例子,在下面的代码getArea方法里,很明显我们的shape.kind不一定是radius,所以不一定可以求圆的面积,这里我们用了强制断言,但这并不是一种最好的处理方法

interface Shape{
    kind:'circle'|'square'
    radius?: number
    sideLength?: number
}
function handleShape(shape: Shape) {
    if (shape.kind === 'square') {
        
    }
}
function getArea(shape:Shape){
    if(shape.kind==='circle'){
        return Math.PI*shape.radius!**2//平方求圆面积,使用断言强制确定该值不为null和undefined
    }
}

经过以下的修改就不会涉嫌危险断言的问题了

这使用了 Discriminated Union 模式

每个接口都有一个共同的判别字段,例如kind

每个接口都有自己特定的必须属性

ts可以根据判别的字段自动收窄类型

可以应用在状态管理上

interface Circle {
  kind: 'circle'
  radius: number
}
interface Square {
  kind: 'square'
  sideLength: number
}
type Shape = Circle | Square

function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
  }
}

never类型与穷尽性检查

never类型标识永远不肯发生的值的类型

用于表示以下情况:

  • 函数永远不会正常返回
  • 类型收窄后不可能存在的分支
  • 不可能发生的情况

never大概是这样用的

interface Circle {
  kind: 'circle'
  radius: number
}
interface Square {
  kind: 'square'
  sideLength: number
}
interface Triangle {
  kind: 'triangle'
  sideLength: number
}
type Shape = Circle | Square | Triangle
function getArea(shape: Shape) {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2
    case 'square':
      return shape.sideLength ** 2
    default:
      // 类型“Shape”上不存在属性“kind”时走此分支
          const _exhaustiveCheck: never = shape //不能将类型“Triangle”分配给类型“never”
          //因为在定义Triangle之前,不是圆和方形的结果走到default必然为never
          //现在定义了Triangle,所以走到default的结果为Triangle,不能赋值给never

      return _exhaustiveCheck
  }
}
  

在ts上如何使用函数

函数类型表达式

长得像箭头函数

大概就是这样

function greet(fn: (a: string) => void) {
  fn('hello')
  //fn的接收值为空
}
function printToConsole(s: string) {
  console.log(s)
  //printToConsole的返回值为空
}
greet(printToConsole)//打印hello

调用签名

调用签名是一种描述函数类型的方法

如果我们想用属性来描述可调用的东西,可以在对象里写一个调用签名

// 1. 普通的函数声明
function add(a: number, b: number): number {
  return a + b;
}

// 2. 调用签名(描述函数类型)
type AddFunction = (a: number, b: number) => number;

注意只有函数表达式赋值给变量后才能添加属性

type DescribableFunction = {
  description: string //属性:函数有一个 description 字符串属性
  (someArg: number): boolean // 调用签名:函数本身接受 number 参数,返回 boolean
} //同时具有属性和调用签名的函数类型定义
function doSomething(fn: DescribableFunction) {
  console.log(fn.description + '的结果是' + fn(100))
} //doSomething的参数fn的类型是DescribableFunction


//function fn(someArg: number) {
//return someArg > 100
//}这样是不行的,要写成函数表达式赋值给变量的形式,例如下面这个

const fn: DescribableFunction = (someArg: number): boolean => {
  return someArg > 100
} //正好fn的类型就是一个DescribableFunction

fn.description = '判断是否大于100'
doSomething(fn) //判断是否大于100的结果是false

构造签名

ts里描述构造函数类型的方式,类似于调用签名,但是用于构造函数

与调用签名的区别其实就是一个用作普通函数,一个用作构造函数

// 调用签名(普通函数)
interface Callable {
  (x: number): string;
}

// 构造签名(构造函数)  
interface Constructable {
  new (x: number): MyClass;
}

举个例子展现构造签名的使用

class Ctor {
  s: string
  constructor(s: string) {
    this.s = s
  }
}

type SomeConstructor = {
  new (s: string): Ctor
}//构造签名
function fn(ctor: SomeConstructor) {
  return new ctor('hello') //ctor相当于一个构造函数,返回这个构造函数的实例
}//接收任何符合SomeConstructor签名的构造函数
console.log(fn(Ctor).s)//hello

结合使用调用签名和构造签名

interface CallOrConstructor {
  new (s: string): Date
  (n: number): number
}
function fn(date: CallOrConstructor) {
    let d = new date('2023-01-01')
    let n = date(100)
}

泛型函数

泛型函数允许我们创建可重用、类型安全的函数,能够处理多种类型

在这里用any就会有问题,因为返回的类型不准确,不安全

function firstElement(arr: any[]) {
  return arr[0]
}
console.log(firstElement(['a', 'b', 'c']))//a,但是a的类型是any而不是string
console.log(typeof firstElement(['a', 'b', 'c']))//在js里是object,在ts里是any

在调用时由ts根据传入的参数自动推断类型

//使用泛型
//此时的T代表这一个类型变量,意为“某种类型”,而函数在调用时会自己推断T的具体类型
function firstElement<T>(arr: T[]) :T|undefined{
  return arr[0]
}
console.log(firstElement(['a', 'b', 'c']))//a,保证了传入和传出的类型匹配
console.log(firstElement([1, 2, 3]))//1,保证了传入和传出的类型匹配
console.log(firstElement([true, false, true]))//true,保证了传入和传出的类型匹配

泛型是推测你的返回值,不过你也可以指定返回类型

//使用泛型
//此时的T代表这一个类型变量,意为“某种类型”,而函数在调用时会自己推断T的具体类型
function firstElement<T>(arr: T[]) :T|undefined{
  return arr[0]
}
console.log(firstElement<string>(['a', 'b', 'c']))//a,保证了传入和传出的类型匹配
console.log(firstElement<number>([1, 2, 3]))//1,保证了传入和传出的类型匹配
console.log(firstElement<boolean>([true, false, true]))//true,保证了传入和传出的类型匹配

泛型不只可以定义一个,还可以定义多个

在这里我们模拟map,设定输入的泛型和输出的泛型的参数,在这里<I>就是string,<O>就是number和string

//模拟map
function map<I, O>(arr: I[], fn: (item: I) => O): O[] {
  return arr.map(fn)
}
console.log(map([1, 2, 3], (item) => item * 2)) //[2, 4, 6]
console.log(map(['a', 'b', 'c'], (item) => item.toUpperCase())) //['A', 'B', 'C']

限制条件

限制条件就是给泛型添加限制,本来是什么类型都可以匹配返回,现在必须保证有限制的那个类型才能返回

function longest<T extends { length: number }>(a: T, b: T) {
  if (a.length >= b.length) {
    return a
  } else {
    return b
  }
}
console.log(longest([1, 2, 3], [4, 5]))//[1, 2, 3]
console.log(longest(['a', 'b', 'c'], ['d', 'e']))//['a', 'b', 'c']
console.log(longest('123', '456'))//'123'
console.log(longest('123', '4567'))//'4567'

使用受限值

当我们对泛型参数使用了约束(extends)后,在函数体内只能访问约束中定义的属性和方法,而不能访问类型参数可能具有的其他属性。

这么写是有问题的,ts这么做是为了防止类型欺骗

function minimumLENGTH<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj
  } else {
    return { length: minimum }//不能将类型“{ length: number; }”分配给类型“Type”。
//Type确实含有length属性,但是length:number并不是Type类型
  } 
}

也可以使用断言,但是也是不建议真正去这样做

function minimumLENGTH<Type extends { length: number }>(
  obj: Type,
  minimum: number
): Type {
  if (obj.length >= minimum) {
    return obj
  } else {
    return { length: minimum } as Type //不能将类型“{ length: number; }”分配给类型“Type”。
    //但是可以使用类型断言将其转换为Type类型
  }
}
const arr = minimumLENGTH([1, 2, 3], 6)
console.log(arr.length) //4

更正确的做法得看是对于什么数据来处理,如果是单纯的数组可以用展开运算符,如果是别的对象就要复制对象了

function minimumArrayLength<T>(arr: T[], minimum: number): T[] {
  if (arr.length >= minimum) {
    return arr;
  } else {
    // 创建新数组,填充 undefined 或默认值
    const newArray = [...arr];
    newArray.length = minimum;
    return newArray;
  }
}

// 使用
const arr = minimumArrayLength([1, 2, 3], 6);
console.log(arr.length); // 6
console.log(Array.isArray(arr)); // true
console.log(arr); // [1, 2, 3, undefined, undefined, undefined]

指定类型参数

在泛型的基础上,指定函数参数的类型

function combine<Type>(arr1: Type[], arr2: Type[]): Type[] {
  return arr1.concat(arr2)
}
//const arr=combine([1,2,3],['string'])这么写是有问题的,因为这里的Type只能容许第一个传入的数组的类型,
//而第二个数组的类型是不能被推断出来的,所以这里需要指定类型参数
const arr = combine<number | string>([1, 2, 3], ['string'])
console.log(arr)//[ 1, 2, 3, 'string' ]

如何编写优秀的通用函数

准则1:

function firstElement<Type>(arr: Type[]): Type | undefined {
  return arr[0]
}//一般建议用类型参数
function lastElement<Type extends any[]>(arr: Type[]): Type | undefined {
  return arr[arr.length - 1]
}//而不是使用extends对其进行约束
const arr = [1, 2, 3]
const first = firstElement(arr)
const last = lastElement(arr)//类型“number[]”的参数不能赋给类型“any[][]”的参数。
//不能将类型“number”分配给类型“any[]”。

准则2:

function filter1<Type>(arr: Type[], callback: (arg: Type) => boolean) {
  return arr.filter(callback)
}
function filter2<Type, Callback extends (arg: Type) => boolean>(//没必要在此处单独定义一个类型参数
  arr: Type[],
  callback: Callback
) {
  return arr.filter(callback)
}

准则3:

function greet(str: string) {
  console.log(str)
}
greet('hello')//更简洁
function greet2<Str extends string>(str: Str) {
  console.log(str)
}
greet2('hello')

总的来说我感觉就是避免使用无谓的限制条件

可选参数

某个参数是可选的,填不填都行的,用问号标明

function f(a: number, b?: number) {//b为可选
  if (b) {
    return a + b
  }
  return a
}

和!的区别是:!通常用于断言某个值一定存在

interface User {
  name?: string; // 可选属性
}

function getUserName(user: User): string {
  return user.name!; // 断言 name 一定存在
}

回调函数的可选参数

回调参数的可选性应该由调用方(你的 myForEach 函数)的行为决定,而不是由回调函数的类型声明随意决定。

function myForEach(arr: any[], callback: (item: any, index?: number) => void) {//不要在回调函数中使用可选参数
  //forEach的回调函数中,不能使用可选参数
  for (let i = 0; i < arr.length; i++) {
    callback(arr[i], i)
  }
}
myForEach([1, 2, 3], (item) => {
  console.log(item)
})
myForEach([1, 2, 3], (item, index) => {
  console.log(item, index)
})

函数重载

重载签名:只有类型声明,没有函数体

实现签名:只有具体的函数代码,实现签名只有一个,并且兼容所有的重载签名

function makeDate(timestamp: number): Date //重载签名
function makeDate(m: number, d: number, y: number): Date //重载签名
function makeDate(m: number, d?: number, y?: number): Date {//实现签名
  if (d !== undefined && y !== undefined) {
    return new Date(y, m, d)
  } else {
    return new Date(m)
  }
}
const d1 = makeDate(1000000000000)
const d2 = makeDate(1000000000000, 10, 10)
//const d3 = makeDate(1000000000000, 10)不能这么写,因为并没有重载签名与之对应

重载签名和实现签名

研究重载签名和实现签名的三部分:参数不正确、参数类型不正确、返回类型不正确

我们只能根据重载签名传入参数,不能通过实现签名

function fn(x: string): void
function fn() {}
fn(hello)

实现签名的参数类型要兼容

function fn(x: boolean): void
function fn(x: string): void
function fn(x: boolean | string) {
    
}

实现前面的返回值类型要与函数重载对应

function fn(x: boolean): boolean
function fn(x: string): string
function fn(x: boolean | string) {
  if (typeof x === 'boolean') {
    return x//返回值满足第一个重载签名的要求
  } else {
    return x//返回值满足第二个重载签名的要求
  }
}

编写优秀的重载

一般建议使用联合类型的参数,而不是重载函数

function len(s: string): number
function len(arr: any[]): number
function len(x: any) {
  return x.length
} //可以使用字符串和数组调用,但是不可以用可能是数组和字符串的类型调用

len('hello')
len([1, 2, 3])
len(Math.random() > 0.5 ? 'hello' : [1, 2, 3])//报错

拯救方法:使用联合类型

function len(x: string | any[]) {
  return x.length
}

len('hello')
len([1, 2, 3])
len(Math.random() > 0.5 ? 'hello' : [1, 2, 3]) //报错

函数内的this声明

和js一样,TS不允许为箭头函数使用this参数

interface User {
  admin: boolean
}

interface DB {
  filterUsers(filter: (this: User) => boolean): User[]
}

const db: DB = {
  filterUsers(filter: (this: User) => boolean): User[] {
    let user1: User = {
      admin: true,
    }
    let user2: User = {
      admin: false,
    }
    return [user1, user2]
  },
}
const admins = db.filterUsers(function (this: User) {
  return this.admin
})//普通函数可以使用this参数

//onst admins2=db.filterUsers((this:User)=>{
//   return this.admin
//})箭头函数不能使用this参数,因为箭头函数没有自己的this,只能通过上下文捕获

console.log(admins)

补充额外类型

在js里如果不写函数的返回值,默认返回undefined,但是在ts里void和undefined是不一样的

参数展开运算符--形参展开

其实就是es6的剩余参数

function multiply(n: number, ...m: number[]) {
  return m.map((x) => x * n)
}
const a = multiply(2, 1, 2, 3)//[1,2,3]归为接收剩余参数的部分
console.log(a)//[ 2, 4, 6 ]

参数展开运算符--实参展开

展开运算符

const arr1 = [1, 2, 3]
const arr2 = [4, 5, 6]
arr1.push(...arr2)
console.log(arr1)//[ 1, 2, 3, 4, 5, 6 ]

//const args = [8, 5]
//const angle = Math.atan2(...args)//错误使用,因为参数不是数组
const args = [8, 5] as const
const angle = Math.atan2(...args); // ✅ 正确

参数解构

其实还是展开运算符的用法

function sum({ a, b, c }: { a: number; b: number; c: number }) {
  console.log(a + b + c)
}

sum({
  a: 10,
  b: 20,
  c: 30,
})//60

把组合类型提出来

//写的简单一点就是这样
type sumType = {
    a: number,
    b: number,
    c: number
}

function sum({ a, b, c }: sumType) {
  console.log(a + b + c)
}

sum({
  a: 10,
  b: 20,
  c: 30,
})//60

返回void类型

type voidFunc = () => void
const f1: voidFunc = () => {
  return true
}
const f2: voidFunc = () => true
const f3: voidFunc = function () {
  return true
}
//这种写法没问题
const v1: void = f1()
const v2: void = f2()
const v3: void = f3()
console.log(v3)//true,即使我们要求返回值为void,也会返回true,只不过我们常常会忽略



// 不能将类型“boolean”分配给类型“void”。
// const v4: boolean = f1()
// const v5: boolean = f2()
// const v6: boolean = f3()
// 这是因为void 类型的变量只能赋值 undefined 和 null,而 boolean 类型的变量可以赋值 true 和 false。
// 所以,当我们将一个 boolean 类型的变量赋值给一个 void 类型的变量时,会报错。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值