TypeScript 学习笔记 -- 高级类型

4,332 阅读7分钟

非空断言操作符 !

用于断言操作对象是非null和非undefined。具体而言,x!将从x值域中排出null和undefined。
简单来说忽略null和undefined类型。

// 断言操作是非null和非undefined
type ListNode = {
    data: number
    next?: ListNode
}
// 用于添加下一个节点
function AddNext(node: ListNode) {
    if (node.next === undefined) node.next = { data: 0 }
}
// 用于设置下一个节点的值
function SetNextValue(node: ListNode, value: number) {
    AddNext(node)
    node.next!.data = value // 断言node.next并不是undefined
}

let list: ListNode = { data: 1 }
SetNextValue(list, 2)
console.log('list', list) // list { data: 1, next: { data: 2 } }
// 网上常见案例 忽略null和undefined
function myFunc(maybeString: string | undefined | null) {
    // Type 'string | null | undefined' is not assignable to type 'string'. (不能将类型“string | null | undefined”分配给类型“string”。)
    // Type 'undefined' is not assignable to type 'string'. (不能将类型“undefined”分配给类型“string”)
    const onlyString: string = maybeString // Error
    const ignoreUndefinedAndNull: string = maybeString! // Ok
}
// 调用函数时忽略 undefined 类型
type NumGenerator = () => number
function myFunc(numGenerator: NumGenerator | undefined) {
    // Object is possibly 'undefined'. (对象可能为“未定义”。)
    // Cannot invoke an object which is possibly 'undefined'. (不能调用可能是“未定义”的对象。)
    const num1 = numGenerator() // Error
    const num2 = numGenerator!() //OK
}

非空断言操作符会从编译生成的JavaScript代码中移除,所以在实际使用的过程中,要特别注意!
非空断言本身是不安全的,主观的判断存在误差,从防御性编程的角度,是不推荐使用非空断言的
因为使用了非空断言,因此编译的时候不会报错,但是运行的时候会报错。

可选链运算符 ?.

变量不为null或undefined返回该对象的属性或方法(优点:省去对象为空的判断代码)

interface Foo {
    [key: string]: {
        baz: () => void
    }
}

let foo: Foo = {
    bar: {
        baz() {
            console.log('this is function baz')
        }
    }
}

// 转换成JavaScript 
// let newFoo = (foo === null || foo === undefined) ? undefined : foo.bar.baz()
let newFoo = foo?.bar.baz()

1. 如果foo.bar是null或undefined,访问仍然出错,所需要在bar加上判断 foo?.bar?.baz()
2. JavaScript中 && 操作符为false的情况('', 0, NaN, false, undefined, null) ?.为false的情况(NaN, false, undefined, null) 空字符串和0是有数据
3. 可用于数组(eg.arr?.[0]) 方法(eg.log?.(log方法若存在 则会输出该说明文字))

空值合并运算符 ??

变量为null或undefined 取??后面的默认值

let foo: null = null
let bar = (): string => {
    return 'this is function bar'
}
// 转换为JavaScript 
// let newFoo = (foo !== null || foo !== undefined) ? foo : bar()
let newFoo = foo ?? bar()
console.log('newFoo', newFoo) // newFoo this is function bar

?? 与 || 的区别

// 当localStorage.volume设置为0时 页面将设置为0.5
let volume = localStorage.volume || 0.5
// 当localStorage.volume设置为0时 页面将设置为0
let volume = localStorage.volume ?? 0.5

?? 直接与 && 或 || 操作符使用时是不恰当的 这种情况会抛出SyntaxError

// 不能在不使用括号的情况下混用 "||" 和 "??" 操作。
null || undefined ?? 'foo' // Error
// 不能在不使用括号的情况下混用 "&&" 和 "??" 操作。
true && undefined ?? 'foo' // Error

// 整体修改为
(null || undefined) ?? 'foo'

与可选链操作符?.的关系

空值合并运算符针对null和undefined这两个值, 可选链操作符?. 也是如此。
可选链操作符,对于访问属性可能为null或undefined的对象时非常有用

interface Customer {
    name: string
    city?: string
}
let customer: Customer = {
    name: 'xiaoming'
}
let customerCity = customer?.city ?? 'Unknown city'

私有字段 #XXX

class Person {
    // 专用标识符仅在面向 ECMAScript 2015 和更高版本时可用。
    #name: string;
    constructor(name: string) {
        this.#name = name;
    }
    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let semlinker = new Person("Semlinker");
semlinker.#name

交叉类型 &

将多个类型合并成一个类型

interface Button {
    type: string
    text: string
}

interface Link {
    alt: string
    href: string
}

const BottonLink: Button & Link = {
    type: 'danger',
    text: '跳转到百度',
    alt: '跳转到百度',
    href: 'http://www.baidu.com'
}

联合类型 |

表示其类型为连接多个类型中的任意一个

interface Button {
    type: 'default' | 'primary' | 'danger'
    text: string
}

const btn: Button = {
    type: 'danger',
    text: '按钮'
}

条件类型(U ? X : Y) 语法规则和三元表达式一致

// 手写 Exclude
type myExclude<T, U extends keyof any> = T extends U ? never : T;

const excludeTest: myExclude<number | string | boolean, number> = "";

类型索引 keyof

类似Object.key 获取一个接口中key的联合类型

interface Button {
    type: string
    text: string
}

type ButtonKeys = keyof Button
// 等效于 type ButtonKeys = 'type' | 'text'

类型约束 extends

在class中使用是继承作用 在泛型中作用是对泛型加以约束

Tip: class想使用interface,需要用implements实现接口。除此以外,interface也可以使用interface,需要用extends继承接口。

type BaseType = string | number | boolean

// 泛型T限制范围为BaseType arg参数传值类型只能为BaseType中的类型
function Copy<T extends BaseType>(arg: T) {
    return arg
}
Copy('this is function Copy')
// extends经常和keyof一起使用 通常想获取对象的值 但是这个对象并不确定
const list = {
    name: 'xiaoming',
    age: 18
}

function getValue<T, P extends keyof T>(obj: T, key: P): T[P] {
    return obj[key]
}

console.log('getValue', getValue(list, 'name')) // getValue xiaoming

类型映射 in

主要作用类型的映射, 遍历已有接口的key或者遍历交叉类型

// 工具类型Readonly举例
type Readonly<T> = {
    // keyof T得到一个联合类型 'name' | 'age
    // P in keyof 相当于执行了一次forEach逻辑 遍历'name' | 'age
    readonly [P in keyof T]: T[P]
}
interface obj {
    name: string
    age: string
}
type ReadOnlyObj = Readonly<obj> // 接口obj中name age都为只读属性

Partial

某个接口类型中定义的属性变成可选

// 源码实现
type Partial<T> = {
    [P in keyof T]?: T[P];
};

Required

某个接口类型中定义的属性变成必选

// 源码实现
type Required<T> = {
    [P in keyof T]-?: T[P];
};

通过 -? 移除了可选属性中的? 使得属性从可选变成必须必选的

Readonly

某个接口类型中定义的属性变成只读

// 源码实现
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

Record<K extends keyof any, T>

接受两个类型变量 Record生成的类型 具有类型K中存在的属性 值为类型T
类型K加了一个类型约束extends keyof any keyof any是什么呢?
大致就是类型K被约束在string | number | symbol 刚好就是对象的索引类型, 也就是类型K只能指定为这几种类型, 根据下面的场景可以更好理解

场景一:
业务代码中经常构造某个对象的数组 但是数组不方便索引 所以我们有时候会把对象的某个字段拿出来作为索引构造一个新对象
假设有个商品列表的数组 要在商品列表中找到商品名为”xx”的商品 我们一般会遍历数组方式在查找 比较繁琐 为了方便 我们会把这个数组改写为对象

interface Goods {
    id: number
    name: string
    price: number
    url: string
}

const goodsMap: Record<string, Goods> = {}
const goodsList: Goods[] = [{
    id: 1,
    name: 'xiaoming',
    price: 100,
    url: 'www.baidu.com'
}]

goodsList.forEach(item => goodsMap[item.name] = item)
console.log('goodsMap', goodsMap) 
// xiaoming: { id: 1, name: 'xiaoming', price: 100, url: 'www.baidu.com' }

场景二: 用于接口Response类型声明

type Car = 'Audi' | 'BMW' | 'MercedesBenz'
type CarList = Record<Car, { price: number }>

const car: CarList = {
    'Audi': { price: 20 },
    'BMW': { price: 30 },
    'MercedesBenz': { price: 40 }
}
console.log('car', car)
// car {
//     Audi: { price: 20 },
//     BMW: { price: 30 },
//     MercedesBenz: { price: 40 }
//   }

Pick<T, K extends keyof T>

泛型T检出指定属性并组成一个新的对象类型
场景:Todo(预览是只需要标题的完成状态) 简单理解从一个对象中提取想要的属性

interface Todo {
    title: string
    completed: boolean
    description: string
}

type TodoPreview = Partial<Pick<Todo, 'title' | 'completed'>>
const todo: TodoPreview = {
    title: 'clean room',
}

Exclude<T, U>

属性排除 泛型T中排除可以赋值给泛型U的类型

type num = 1 | 2 | 3
type numExclude = Exclude<num, 1 | 2>
const numList: numExclude = 3
interface Worker {
    name: string
    age: number
    email: string
    salary: number
}

interface Student {
    name: string
    age: number
    email: string
    grade: number
}

type ExcludeKeys = Exclude<keyof Worker, keyof Student>
// 取出的是Worker在Student中不存在的 salary
let ExcludeKeysList: ExcludeKeys = 'salary'

与Pick相反,Pick用于拣选出我们需要关心的属性,而Exclude用于排除掉我们不需要关心的属性,Exclude很少单独使用,可以与其它类型的配合实现更复杂更有用的功能

Extract<T, U>

泛型T中提取可以赋值给泛型U的类型

type num = 1 | 2 | 3
type numExclude = Extract<num, 1 | 2>
const numList: numExclude = 1 // 1或者2

Omit<T, K extends keyof any>

从泛型T提取不在泛型K中的属性类型 并组成一个新的对象类型

interface IUser {
    name: string
    age: number
    address: string
}

type IUserOmit = Omit<IUser, 'name' | 'age'>
// 忽略掉IUser接口中的name和age属性
let iUserOmit: IUserOmit = {
    address: 'shenzhen'
}
console.log('iUserOmit', iUserOmit) // iUserOmit { address: 'shenzhen' }

NonNullable

从泛型T中排除null和undefined

type U = NonNullable<'name' | undefined | null>;
let text: U = 'name'

此外还有
Parameters<T extends (...args: any) => any>
ConstructorParameters<T extends new (...args: any) => any>
ReturnType<T extends (...args: any) => any>
InstanceType<T extends new (...args: any) => any>
等等工具类型,工具类型大大提高了类型的可拓展能力,方便复用。