特殊类型
特殊类型
any类型
基本意义
any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。
let x: any;
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确上面示例中,变量x的类型是any,就可以被赋值为任意类型的值。 变量类型一旦设为any
,TypeScript实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。
let x: any = 'hello';
x(1) // 不报错
x.foo = 100; // 不报错上面示例中,变量x的值是一个字符串,但是把它当作函数调用,或者当作对象读取任意属性,TypeScript 编译时都不报错。
原因就是x的类型是any,TypeScript 不对其进行类型检查。由于这个原因,应该尽量避免使用any类型,否则就失去了使用 TypeScript 的意义。 实际开发中,any类型主要适用以下两个场合。
出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为
any。为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为
any。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上any,TypeScript 编译时就不会报错。
总之,TypeScript 认为,只要开发者使用了any类型,就表示开发者想要自己来处理这些代码,所以就不对any类型进行任何限制,怎么使用都可以。 从集合论的角度看,any类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。类型推断问题
对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是any。function add(x, y) { return x + y; } add(1, [1, 2, 3]) // 不报错
上面示例中,函数add()的参数变量x和y,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是any。以至于后面就不再对函数add()进行类型检查了,怎么用都可以。 这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为any。 TypeScript 提供了一个编译选项noImplicitAny,打开该选项,只要推断出any类型就会报错。
$ tsc --noImplicitAny app.ts上面命令使用了noImplicitAny编译选项进行编译,这时上面的函数add()就会报错。 这里有一个特殊情况,即使打开了noImplicitAny,使用let和var命令声明变量,但不赋值也不指定类型,是不会报错的。
var x; // 不报错
let y; // 不报错上面示例中,变量x和y声明时没有赋值,也没有指定类型,TypeScript 会推断它们的类型为any。这时即使打开了noImplicitAny,也不会报错。
let x;
x = 123;
x = {foo: 'hello'};上面示例中,变量x的类型推断为any,但是不报错,可以顺利通过编译。 由于这个原因,建议使用let和var声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。 const命令没有这个问题,因为 JavaScript 语言规定const声明变量时,必须同时进行初始化(赋值)。const x; // 报错 上面示例中,const命令声明的x是不能改变值的,声明时必须同时赋值,否则报错,所以它不存在类型推断为any的问题。
- 污染问题
any类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。上面示例中,变量x的类型是any,实际的值是一个字符串。变量y的类型是number,表示这是一个数值变量,但是它被赋值为x,这时并不会报错。然后,变量y继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。 污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用any类型的另一个主要原因。let x:any = 'hello'; let y:number; y = x; // 不报错 y * 123 // 不报错 y.toFixed() // 不报错
Unknown 类型
为了解决any类型“污染”其他变量的问题,TypeScript 3.0 引入了unknown类型。它与any含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像any那样自由,可以视为严格版的any。 unknown跟any的相似之处,在于所有类型的值都可以分配给unknown类型。
let x: unknown;
x = true; // 正确
x = 42; // 正确
x = 'Hello World'; // 正确上面示例中,变量x的类型是unknown,可以赋值为各种类型的值。这与any的行为一致。 unknown类型跟any类型的不同之处在于,它不能直接使用。主要有以下几个限制。 首先,unknown类型的变量,不能直接赋值给其他类型的变量(除了any类型和unknown类型)。
let v: unknown = 123;
let v1: boolean = v; // 报错let v2:number = v; // 报错上面示例中,变量v是unknown类型,赋值给any和unknown以外类型的变量都会报错,这就避免了污染问题,从而克服了any类型的一大缺点。 其次,不能直接调用unknown类型变量的方法和属性。
let v1: unknown = {foo: 123};
v1.foo // 报错let v2:unknown = 'hello';
v2.trim() // 报错let v3:unknown = (n = 0) => n + 1;
v3() // 报错上面示例中,直接调用unknown类型变量的属性和方法,或者直接当作函数执行,都会报错。 再次,unknown类型变量能够进行的运算是有限的,只能进行比较运算(运算符==、=、!=、!、||、&&、?)、取反运算(运算符!)、typeof运算符和instanceof运算符这几种,其他运算都会报错。
let a: unknown = 1;
a + 1 // 报错
a === 1 // 正确上面示例中,unknown类型的变量a进行加法运算会报错,因为这是不允许的运算。但是,进行比较运算就是可以的。
那么,怎么才能使用unknown类型变量呢?
答案是只有经过“类型缩小”,unknown类型变量才可以使用。所谓“类型缩小”,就是缩小unknown变量的类型范围,确保不会出错。
let a: unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}上面示例中,unknown类型的变量a经过typeof运算以后,能够确定实际类型是number,就能用于加法运算了。这就是“类型缩小”,即将一个不确定的类型缩小为更明确的类型。
下面是另一个例子。
let s: unknown = 'hello';
if (typeof s === 'string') {
s.length; // 正确
}上面示例中,确定变量s的类型为字符串以后,才能调用它的length属性。
这样设计的目的是,只有明确unknown变量的实际类型,才允许使用它,防止像any那样可以随意乱用,“污染”其他变量。类型缩小以后再使用,就不会报错。
总之,unknown可以看作是更安全的any。一般来说,凡是需要设为any类型的地方,通常都应该优先考虑设为unknown类型。
在集合论上,unknown也可以视为所有其他类型(除了any)的全集,所以它和any一样,也属于 TypeScript 的顶层类型。
Never 类型
为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。 由于不存在任何属于“空类型”的值,所以该类型被称为never,即不可能有这样的值。
let x: never;上面示例中,变量x的类型是never,就不可能赋给它任何值,否则都会报错。
never类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。另外,不可能返回值的函数,返回值的类型就可以写成never,详见《函数》一章。
如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于never类型。
function fn(x: string | number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}上面示例中,参数变量x可能是字符串,也可能是数值,判断了这两种情况后,剩下的最后那个else分支里面,x就是never类型了。 never类型的一个重要特点是,可以赋值给任意其他类型。
function f(): never {
throw new Error('Error');
}
let v1: number = f(); // 不报错
let v2: string = f(); // 不报错
let v3: boolean = f(); // 不报错上面示例中,函数f()会抛出错误,所以返回值类型可以写成never,即不可能返回任何值。各种其他类型的变量都可以赋值为f()的运行结果(never类型)。
为什么never类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了never类型。因此,never类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。
总之,TypeScript 有两个“顶层类型”(any和unknown),但是“底层类型”只有never唯一一个。
symbol
Symbol 是ES2015 新引入的一种原始类型的值。它类似字符串,但是每一个Symbol值都是独一无二的, 与其他任何值都不相等。
Symbol 值通过Symbol()函数生成。在 TypeScript 里面,Symbol 的类型使用symbol表示。
let x: symbol = Symbol();
let y: symbol = Symbol();
x === y // falseunique symbol
symbol类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值。
比如,5是一个具体的数值,就用5这个字面量来表示,这也是它的值类型。但是,Symbol 值不存在字面量,必须通过变量来引用,所以写不ss出只包含单个 Symbol 值的那种值类型。
为了解决这个问题,TypeScript 设计了symbol的一个子类型unique symbol,它表示单个的、某个具体的 Symbol 值。
因为unique symbol表示单个值,所以这个类型的变量是不能修改值的,只能用const命令声明,不能用let声明。
// 正确
const x: unique symbol = Symbol();
// 报错
// let y:unique symbol = Symbol();const命令为变量赋值 Symbol 值时,变量类型默认就是unique symbol,所以类型可以省略不写。
const x: unique symbol = Symbol();
// 等同于const x = Symbol();每个声明为unique symbol类型的变量,它们的值都是不一样的,其实属于两个值类型。
const a: unique symbol = Symbol();
const b: unique symbol = Symbol();
a === b // 报错上面示例中,变量a和变量b的类型虽然都是unique symbol,但其实是两个值类型。不同类型的值肯定是不相等的,所以最后一行就报错了。
由于 Symbol 类似于字符串,可以参考下面的例子来理解。
const a: 'hello' = 'hello';
const b: 'world' = 'world';
a === b // 报错上面示例中,变量a和b都是字符串,但是属于不同的值类型,不能使用严格相等运算符进行比较。
而且,由于变量a和b是两个类型,就不能把一个赋值给另一个。
const a: unique symbol = Symbol();
const b: unique symbol = a; // 报错例变量b的类型,如果要写成与变量a同一个unique symbol值类型,只能写成类型为typeof a。
const a: unique symbol = Symbol();
const b: typeof a = a; // 正确不过我们知道,相同参数的Symbol.for()方法会返回相同的 Symbol 值。TypeScript 目前无法识别这种情况,所以可能出现多个 unique symbol 类型的变量,等于同一个 Symbol 值的情况。
const a: unique symbol = Symbol.for('foo');
const b: unique symbol = Symbol.for('foo');unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行。
const a: unique symbol = Symbol();
const b: symbol = a; // 正确
// const c:unique symbol = b; // 报错unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol,不能是 symbol。
const x: unique symbol = Symbol();
const y: symbol = Symbol();
interface Foo {
[x]: string; // 正确
[y]: string; // 报错
}上面示例中,变量y当作属性名,但是y的类型是 symbol,不是固定不变的值,导致报错。
unique symbol类型也可以用作类(class)的属性值,但只能赋值给类的readonly static属性。
class C {
static readonly foo: unique symbol = Symbol();
}上面示例中,静态只读属性foo的类型就是unique symbol。注意,这时static和readonly两个限定符缺一不可,这是为了保证这个属性是固定不变的。
类型推断
如果变量声明时没有给出类型,TypeScript 会推断某个 Symbol 值变量的类型。
let命令声明的变量,推断类型为 symbol。
// 类型为 symbol
let x = Symbol();const命令声明的变量,推断类型为 unique symbol。
// 类型为 unique symbol
const x = Symbol();但是,const命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。
let x = Symbol();
// 类型为 symbol
const y = x;let命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。
const x = Symbol();
// 类型为 symbol
let y = x;