TS 入门——类型约束

243 阅读7分钟

前言

ts是js的超集,是一门静态强类型语言,它重点对js的类型做了一个很大的规范和约束。为了方便记忆,我将所有类型分为三类:

  • "三傻":any unknown never
  • js 中就有的类型:七种原始类型 + 对象
  • ts引入的特殊类型:值类型 联合类型 交叉类型
  • 复杂类型:Array、tuple、Function、Object等

any、unknown 和 never

  • 一个变量被声明成any类型,那么这个变量的用法跟在 js 中一样,可以被赋予任意类型的值,也可以赋给任何类型的其他变量 ( 除了never )
let a: any = 1
let b: string = 'hello'

b = a  // 不报错
  • 一个变量被声明成undefined类型,那么这个变量可以被赋予任意类型的值,与 any 不同的是:不能赋给其他类型的变量
let a: unknown = 2
let b: string
b = a  // 报错!

unknown 类型的变量做任何事都是不合法的

let a: unknown = 2
let b = a + 1  // 报错!

不能访问 unknown 类型值上的属性

let obj: unknown
obj = {a: 1, b: 2}
console.log(obj.a);  // 报错:类型“unknown”上不存在属性“a
  • never 表示那些永远不可能存在的值的类型。

never是任何类型的子类型,可以赋值给任何类型;但所有类型都不是它的子类型,没有类型可以赋值给 never 类型(除了 never 本身之外)

修饰没有返回值的函数:

function throwError(message: string): never {
    throw new Error(message);  // 抛出异常
}

function loopForever(): never {
    while (true) {}  // 无限循环
}

经过一系列类型守卫之后仍然不能确定其具体类型时,使用never类型表明代码中可能存在逻辑错误

function combineNames(first: string | number, last: string | number): string {
    if (typeof first === 'string' && typeof last === 'string') {
        return first + ' ' + last;
    } else if (typeof first === 'number' && typeof last === 'number') {
        // 这里可能是错误的逻辑,因为我们需要返回字符串
        return first.toString() + last.toString();
    } else {
        // 如果我们没有处理所有情况,这里的类型将是 never
        const neverHappens: never = first; // 或者是 last
        return neverHappens.toString(); // 这行不会被执行
    }
}

使用never 类型可以在类型系统中确保所有的可能性都被考虑到了,尽早发现逻辑错误,并提高代码质量

七种原始类型 + 对象

const a: boolean = true
const b: string = 'hello'
const c: number = 1
const d: symbol = Symbol(123)
const e: bigint = 1234n

const obj: object = {}
const fn: object = function() {}
const arr: object = []

const f: undefined = undefined
const g: null = null

let o: Object   // new一个构造函数出来的对象,这个对象的类型就是大写的某某,比如String、Number等,区分于string、number
o = true   // 不报错:因为万物皆对象
o = 123

在 tsconfig.json 配置文件中, 若 noImplicitAny 设置为 false 时,TypeScript 会允许隐式 any 类型的存在,如果你没有为变量指定类型,会默认它们为 any 类型;反之设置为 true 时,TypeScript 会禁止隐式 any 类型的存在,如果你没有为变量指定类型,TypeScript 会报错,提示你需要显式地指定类型。

所以:

"noImplicitAny": false时,undefined 和 null 会被视为 any 类型 "noImplicitAny": true时,undefined 被推断为 unknown,null 被推断为 null

symbol

Symbol 就是用来创建这个世界上独一无二的值。

ts 中 symbol 类型有个特殊的用法,就是与 unique 联合使用时,变量必须用 const 声明,此时该变量的值后续不能再作修改

const z: unique symbol = Symbol()

其实也跟不显式声明类型的效果一样,const z = Symbol(),变量 z 也是不能再修改值的

ts引入的特殊类型

  • 值类型
let x: 'hello'  // x只能被声明为'hello'类型
x = 'hello'
x = 'world' // 报错!
  • 联合类型:表示一个值类型是多种类型之一
let y: string | number
y = 1
y = 'hello'

联合值类型,可以用来参数或变量为特定的那几个类型

function color(color: '红' | '橙' | '黄' | '绿'){}
color('红')
color('黄')
color('绿')
color('hello')  // 报错!
  • 交叉类型:表示一个值同时具有多个类型的特性
let obj: {foo: string} & {bar: number}
obj = {
    foo: 'hello',
    bar: 1
}
  • type 关键字:可以自定义类型
type Common = string | number | boolean   
// 可以看做封装了一下,想要联合类型的时候就不用搬着长长一段到处搬了

使用模板字符串来构建字符串类型:

举例——Greeting 是一个模板字面量类型,它由静态部分 "hello " 和动态部分 ${World} 组成。

type World = "world" | "everyone";
type Greeting = `hello ${World}`;

function greet(who: World): Greeting {
    return `hello ${who}`;
}

console.log(greet("world"));  // 输出 "hello world"
console.log(greet("everyone"));  // 输出 "hello everyone"

// 以下代码将导致类型错误,因为 "friend" 不是 World 类型的一部分
// console.log(greet("friend"));  // 错误

复杂类型

Array

原生js中的数组是可以存放多种类型的值的,但是ts中的数组被常规声明成了某种类型后,其中的元素就全部只能是那种指定类型,比如:let arr: number[] = [1, 2, 3],arr中就只能存放数字类型,否则编译就会报错

  • 如果要存放不同类型的值,可以使用联合类型,比如:let arr2: (number | string | boolean)[] = [1, '2', true]

  • 也可以使用泛型来声明类型

let arr: Array<number> = []
let arr2: Array<number | string | boolean> = [1, '2', true]
  • 使用readonly可以将变量声明成只读,比如const arr: readonly number[] = [1, 2, 3],数组arr就不能被增删改了。注意,只能对数组和元组的字面量使用 readonly 类型修饰符,泛型定义的数组不能使用

若要对泛型定义的数组限制只读,两种方式:

let arr1: ReadonlyArray<string> = ['1', '2']
let arr2: Readonly<number[]> = [1, 2]

const在 ts 中也可以被用来限制只读:const arr = [1, 2] as const,同样该数组也不能被增删改了

tuple

在ts开发过程中,我们确实经常要在数组中放各种类型的元素,此时比数组更加适用的就是元组。

元组与数组看上去最大的一个区别就是元组的成员类型是放在方括号[ ]里面的,比如:const tuple: [number, string, boolean] = [1, '2', true]。并且元素对应的类型顺序不能乱!

元组是不存在类型推断的,如果一个元组不写明类型,那么这个元组会被编译器自动推断为数组

  • 如果类型声明 string 后面接一个,那么元组a 中可以不存在 string 类型对应的值;修饰的类型必须放在所有类型的末位,也就是说不能写成 [string?, number]
let a: [number, string?] = [1]

同样的,如果觉得声明一长串类型不方便用,可以先用 type 自定义一个类型,之后直接拿着你设定的那个类型去用。

  • 元组中类型声明和值一定要一一对应,比如 let x: [number] = [1, 2]是会报错的,正确写法是
let x: [number, number] = [1, 2]
// 或者
let x: [...number[]] = [1, 2]
  • 对元组限制只读
type read = readonly [number, number]
type read2 = Readonly<[number, number]>

let x: read = [1, 2]
let y: read2 = [1, 2]

Function

ts 中函数的使用规则和 js 中差不多,只是 ts 中在操作一个函数时,对参数以及返回值的类型限制会严谨很多,这个在实际开发过程中能自行感受出来

  • 函数重载:允许定义多个具有相同名称但参数列表或返回类型不同的函数签名

函数重载通常由两部分组成:

  1. 多个函数签名(不包含函数体)
  2. 单一的函数实现(包含函数体),该实现要能兼容所有的函数签名
function reverse(str: string): string 
function reverse(str: number[]): number[]

function reverse(str: string | number[]): string | number[] {
    if (typeof str === 'string') {
        return str.split('').reverse().join('')
    } else {
        return str.slice().reverse()
    }
}

console.log(reverse('hello'));
console.log(reverse([1, 2, 3]));

Object

对象的比较常用且简单的使用方法:const obj: {a: number, b: number} = { a:1, b: 2}

ts 中要注意区分 objectObject 的区别:

  • 小写的 object 用来表示任何非原始类型的值。所以用它仅仅是用来表示一个变量不是原始值而已
  • 大写的 Object 是所有 Object类的实例的类型。如果我们去 new 一个实例对象,那么这个实例对象的类型就是大写的 Object

结语

实际上 ts 比 js 真正多出来的基本类型就是any、unknown 和 never 这“三傻”,还有值类型、自定义的type类型这些特殊类型,这样分类来记还是很好记的。