likes
comments
collection
share

TypeScript语法细节

作者站长头像
站长
· 阅读数 58

联合类型

TypeScript的类型系统允许我们使用多种运算符,从现有类型中构建新类型。

联合类型:

  • 联合类型是由两个或者多个其他类型组成的类型;
  • 表示可以是这些类型中的任何一个值;
  • 联合类型中的每一个类型被称之为联合成员(union's members) ;
function printId(id: number | string) {
    console.log("你的id是:", id);
}

printId(10)
printId("abc")

即可传入number类型,也可以传入string类型,通过|运算符来分割。

联合类型别名的使用

在前面,我们通过在类型注解中编写 对象类型 和 联合类型,但是当我们想要多次在其他地方使用时,就要编写多次。

比如我们可以给对象类型起一个别名:

// 类型别名:type
type MyNumber = number // 给number类型,取一个别名

const age: MyNumber = 18

// 给ID的类型起一个别名
type IDType = number | string

function printId(id: IDType) {
    console.log(id);
}

type PointType = { x: number, y: number, z?: number }
// point是一个对象类型,并且指定里面的每个值的类型
function printCoordinate(point: PointType) {
    console.log(point.x, point.y, point.z);
}

接口的声明

此处都是针对【对象类型】的!!!

在前面我们通过type可以用来声明一个对象类型:

type Point = {
    x:number
    y:number
}

对象的另外一种声明方式就是通过接口来声明:

interface Point{
    x:number
    y:number
}

那么它们有什么区别呢?

  • 类型别名和接口非常相似,在定义对象类型时,大部分时候,你可以任意选择使用。
  • 接口的几乎所有特性都可以在 type 中使用(后续我们还会学习interface的很多特性);

interface和type的区别

我们会发现interface和type都可以用来定义对象类型,那么在开发中定义对象类型时,到底选择哪一个呢?

  • 如果是定义非对象类型,通常推荐使用type,比如Direction、 Alignment、一些Function;

如果是定义对象类型,那么他们是有区别的:

  • interface可以重复的对某个接口来定义属性和方法;
  • 而type定义的是别名,别名是不能重复的;
// 区别1:type类型的使用范围更广,接口类型只能用来声明对象
type MyNumber = number
type IDType = number | string

// 区别2:在声明对象时,interface可以多次,type则不可以声明2个同名
interface PointType {
    x: number,
    y: number
}

interface PointType {
    z: string
}

但是当我们使用这个接口的时候,必须2个接口同时满足,否则就会报错!

const point: PointType = {
    x: 100,
    y: 200,
    z: "abc"
}
// interface是支持继承的
interface IPerson {
    name: string,
    age: number
}

interface IKun extends IPerson {
    slogan: string
}

// 必须同时满足IPerson和IKun接口
const ikun1: IKun = {
    name: "kobe",
    age: 18,
    slogan: "你干嘛"
}

接口还有很多增强的属性,我们后续再说,比如继承与实现...

总结:非对象类型的定义,推荐使用type;对象类型的声明,推荐使用interface

交叉类型

  • 交叉类似表示需要满足多个类型的条件;
  • 交叉类型使用&符号;

但是有同时满足是一个number又是一个string的值吗?其实是没有的,所以NewType其实是一个never类型;

type NewType = number & string

所以,在开发中,我们进行交叉时,通常是对【对象类型】进行交叉的:

interface IKun {
    name: string,
    age: number
}

interface ICoder {
    height: number

    // 函数类型
    coding: () => void
}

// info对象,必须 同时满足IKun接口和ICoder接口
const info: IKun & ICoder = {
    name: "abc",
    age: 18,
    height: 1.88,
    coding: () => {
        return 0
    }
}

类型断言as

有时候TypeScript无法获取具体的类型信息,这个我们需要使用类型断言(Type Assertions)。

// 获取DOM元素
// 使用类型断言:我能确定拿到的是HTMLImageElement类型,就不用类型缩小了
const imgEl = document.querySelector(".img") as HTMLImageElement
imgEl.src = "xxx"
imgEl.alt = "uuu"

TypeScript只允许类型断言转换为 【更具体】 或者 【不太具体(any/unknown)】 的类型版本,此规则可防止不可能的强制转换:

但是并不意味着,我们可以乱断言,一定要在自己确定但是TS不能推断出来的时候,或者一个都不确定的,才可以断言;

非空类型断言!

通过?.可选链语法:

// 定义接口
interface IPerson {
    name: string
    age: number

    // 可选的,可以有friend属性,也可以没有
    friend?: {
        name: string
    }
}

const info: IPerson = {
    name: "why",
    age: 18
}

// 这样取是会报错的,由于friend属性是可选的,不知道是否有friend属性;
console.log(info.friend.name);

// 如果使用可选链 ?. 是可以的;
console.log(info.friend?.name);

可选链存在的问题:

// 但是使用可选链,是不可以赋值的
info.friend?.name = "哈哈"

TypeScript语法细节

使用非空类型断言:!.来实现非空类型断言,表示可以确定某个标识符是有值的, 跳过ts在编译阶段对它的检测;

  • 我这里一定是有值的,TS你别检验我,但是这种做法很危险,只有确保friend一定有值的情况,才能使用
info.friend!.name = "janmes"

就是告诉TS,这里一定是会有值的!

字面量类型

  • 值和字面量相同,也就是值必须是字面量
// 1.字面量类型的基本使用
const name: "why" = "why"
let age: 18 = 18

默认情况下这么做是没有太大的意义的,但是我们可以将多个类型联合在一起;

TypeScript语法细节

  • d1的值必须和字面量相同,也就4选1
type Direction = "left" | "right" | "up" | "down"

// 是这4个方向中的其中一个(类似于枚举)
const d1: Direction = "left"
  1. 例子:
// 封装请求方法
type MethodType = "get" | "post"
function request(url: string, method: MethodType) {
    要求url传入string类型,method是get、post
}


// TS细节
const info = {
    url: "xxx",
    method: "post"
}

// 这种做法是错误的,因为info.method获取的是string类型
// request(info.url, info.method)
  1. 解决方法:类型断言
request(info.url, info.method as "post")
  1. 解决方法:字面量
const info2: { url: string, method: "post" } = {
    url: "xxx",
    method: "post"
}

request(info2.url, info2.method)
  1. 字面量推理:

TypeScript语法细节

// as const:推理成字面量类型
const info3 = {
    url: "xxx",
    method: "post"
} as const

类型缩小

  • 我们可以通过类似于 typeof padding === "number" 的判断语句,来改变TypeScript的执行路径;
  • 在给定的执行路径中,我们可以缩小比声明时更小的类型,这个过程称之为 缩小( Narrowing ) ;
  • 而我们编写的 typeof padding === "number" 可以称之为 类型保护(type guards) ;

常见的类型保护有如下几种:

  • typeof
  • 平等缩小(比如===、 !==)
  • instanceof
  • in
  • 其他...

typeof(使用的最多)

在 TypeScript 中,检查返回的值typeof是一种类型保护:

  • 因为 TypeScript 对如何typeof操作不同的值进行编码。
function printID(id: number | string) {
    // 确定是字符串类型
    if (typeof id === "string") {
        console.log(id.length, id.split(" "));
    } else {
        console.log(id);
    }
}

printID("aaa bbb")

平等缩小

我们可以使用Switch或者相等的一些运算符来表达相等性(比如===, !==, ==, and != ):

TypeScript语法细节

instanceof

JavaScript 有一个运算符来检查一个值是否是另一个值的“实例”:

function printDate(date: string | Date) {
    if (date instanceof Date) {
        console.log(date.getTime());

    } else {
        console.log(date);
    }
}

in

Javascript 有一个运算符,用于确定对象是否具有带名称的属性: in运算符

  • 如果指定的属性在指定的对象或其原型链中,则in 运算符返回true;

TypeScript语法细节

判断某一个对象中,是否包含这个key

TypeScript函数类型

function foo(arg: number): number {
    return 111;
}

js中的写法:
const bar = function(arg){
    return 数值类型
}

ts中的写法:
const bar = (arg: number): number => {
    return 111;
}

在JavaScript开发中,函数是重要的组成部分,并且函数可以作为一等公民(可以作为参数,也可以作为返回值进行传递)

  • 那么在使用函数的过程中,函数是否也可以有自己的类型呢?
  • 我们可以编写函数类型的表达式(Function Type Expressions),来表示函数类型

在js中的函数类型表达式:

function calc(calcFn){
    const num1 = 10
    const num2 = 20
    
    const res = calcFn(num1,num2)
    console.log(res)
}

function add(num1,num2){
    return num1+num2;
}

calc(add)

add方法作为参数calcFn传入到calc方法中,然后执行参数方法,并得到结果。即:由外部决定函数的具体操作,内部只是单纯的定义数值。

在ts中的写法:规定传入函数的类型

type CalcType = (num1:number,num2:number)=>number

function calc(calcFn:CalcType){
    const num1 = 10
    const num2 = 20
    const res = calcFn(num1,num2)
    console.log(res)
}

function sum(num1:number,num2:number){
    return num1+num2
}

calc(sum)

此时我的sum函数就符合了CalcType的要求,就可以作为一个参数进行传递。

使用匿名函数

calc(function(num1,num2){
    return num1+num2
})

只要符合要求都可以作为参数传递,并且匿名函数最好不要自己指定类型,交给TS推导是最好的!

function foo(num1:number){
    reuturn num1;
}

calc(foo)

但是我们在这里发现,calc函数要求的函数类型是要2个参数,但是这里foo函数只有1个参数,居然也是可以的!

TypeScript对传入的函数类型的参数个数不进行校验

调用签名

在 JavaScript 中,函数除了可以被调用,自己也是可以有属性值的。

  • 然而前面讲到的函数类型表达式并不能支持声明属性;
  • 如果我们想描述一个带有属性的函数,我们可以在一个对象类型中写一个调用签名(call signature) ;

简单来说就是:我们在之前使用函数类型表达式只能表示出这是一个函数,但不能表示出这个函数里面可能还会存在的其他值:并且函数签名表示一个类型为函数时,不再使用=>,而是:,例如:(num1: number): number

type BarType = (num1: number) => number

// 调用签名:从对象的角度来看待这个函数,也可以有其他属性
interface IBar {
    name: string
    age: number
    // 函数可以调用:函数调用其那面
    // (参数列表): 返回值类型
    (num1: number): number
}

const bar: IBar = (num1: number): number => {
    return 111
}

那么这个bar函数,除了表示接收一个数值参数及返回值为数值以外
还可以说明这个函数还有2个自带的属性:name和age,这个函数被表达的就会更加的充分和具体

bar.name = "aaa"
bar.age = 18
bar(123)
  • 如果只是描述函数类型本身(函数可以被调用),那么就使用函数类型表达式;
  • 如果在描述函数作为对象,可以被调用,同时还具有其他属性时,使用函数调用签名;

构造签名(理解一下即可)

JavaScript 函数也可以使用 new 操作符调用,当被调用的时候, TypeScript 会认为这是一个构造函数(constructors),因为 他们会产生一个新对象。

  • 你可以写一个构造签名( Construct Signatures ), 方法是在调用签名前面加一个 new 关键词;

总而言之就是:当我的构造函数作为参数传递时,我希望TS能够识别出我是构造函数,而不是一个普通函数,那么这个时候就要用到构造签名

并且构造函数也可以有自己的属性,也就是说他也可以通过调用签名描述属性:注意描述函数时需要加上new

class Person {
}

interface ICTORPerson {
    // 声明函数时,需要加上new
    new(): Person
}

-----------------------

// 构造签名
function factory(fn: ICTORPerson) {
    // 我就可以new啦,需要通过new调用哦,并且返回类型是Person
    const f = new fn()
    return f
}

// 这样我就可以传递啦,Person也是一个类,也可以传递一个构造器,都是一样类型的
factory(Person)

常见于第三方工具中,开发中很少用到。

参数的可选类型

TypeScript语法细节

function foo(x:number,y?:number){
    if(y!==undefined){
        ...
    }
}
foo(10)
foo(10,20)

默认参数

TypeScript语法细节

function foo(x:number,y=100){
    ...
}

foo(10)
foo(20,undefined)

如果有默认值参数,是可以传入一个undefined的,这种是可以的,但是几乎不会用到。

剩余参数

TypeScript语法细节

重载

TypeScript语法细节

// 1.先编写重载签名(不需要实现体,因为通用的写过了)
function add(a1: number, a2: number): number
function add(a1: string, a2: string): string

// 2.然后编写通用的函数实现
function add(a1: any, a2: any): any {
    return a1 + a2
}

// 我希望可以传入数字或字符串
add(10, 20)
add("abc", "cba")

但是通用函数是不可以调用的!!!

TypeScript语法细节

联合类型和重载

我们现在有一个需求:定义一个函数,可以传入字符串或者数组,获取它们的长度。

这里有两种实现方案:

  • 方案一:使用联合类型来实现;
  • 方案二:实现函数重载来实现;
// 函数的重载:
/* 
    function getLength(arg: string): number
    function getLength(arg: any[]): number
    function getLength(arg) {
        return arg.length
    } 
*/

// 联合类型的实现
function getLength(arg: string | any[]) {
    return arg.length
}

getLength("aaaaaa")
getLength(["abc", "cba", "nba"])

在开发中我们选择使用哪一种呢?

  • 在可能的情况下,尽量选择使用联合类型来实现;
  • 重载的那个例子不可以用联合类型,因为我怎么知道你的相加是数字类型相加还是字符串类型相加?所以只有当联合类型不能实现的时候,再用重载!

可推导this类型

TypeScript语法细节

我们发现在对象和函数中都是可以正常使用this的,在没有指定this指向时,this的类型是any类型

在没有对TS进行特殊配置的情况下,this是any类型的;

如果我们通过xxx.call(指定this指向,传递的参数)来指定this的话是会改变this的。

我们可以创建一个tsconfig.json文件(tsc --init),并且在其中告知VSCodethis必须明确执行(不能是隐式的);

在设置了noImplicitThis为true时,TypeScript会根据上下文推导this,但是在不能正确推导时,就会报错,需要我们明确的指定this。

TypeScript语法细节

  • 函数的第一个参数我们可以根据该函数之后被调用的情况,用于声明this的类型(名词必须叫this);
  • 在后续调用函数传入参数时,从第二个参数开始传递的, this参数会在编译后被抹除;
function foo(this:{name:string}){
    ...
}

TypeScript语法细节

这个类型不返回一个转换过的类型,它被用作标记一个上下文的this类型。

this的内置工具

TypeScript语法细节

function foo(this:{name:string},info:{name:string}){
    console.log(this,info)
}

// 拿到foo函数的类型:FooType
type FooType = typeof foo

// 使用ThisParameterType:获取FooType类型中this的类型
type FooThisType = ThisParameterType<FooType>

// 使用OmitThisParameter:删除this参数类型,剩余的函数类型
type PureFooType = OmitThisParameter<FooType>

TypeScript语法细节

ThisType:用于绑定一个上下文的this,但是官方文档给出的解释比较含糊

TypeScript语法细节

coderwhy谈TS

TypeScript语法细节

转载自:https://juejin.cn/post/7160650280982282247
评论
请登录