Type与Interface
上述使用的类型基本都是 JS
或 TS
帮我们定义好的,或者说是内置的基础类型。
但是,它们都比较单一,光它们可不能满足我们实际的业务场景,我们可能还要一些更加复杂的类型。
TS
给开发者提供了两种创建复杂类型的方式:type
与 interface
。
Type
type 修饰符用来定义一个类型的别名。
javascript
type Name = string;
type Age = number;
type Obj = {
name: string;
age: number;
};
let name: Name = '橙某人';
let age: Age = 18;
let obj: Obj = {
name: '橙某人',
age: 18,
}
值类型
在 TS
中定义类型的时候,需要开发者时刻注意 "类型" 和 "值类型" 两者不一样的。
javascript
type Name = '橙某人';
type Age = 18;
type Obj = {
name: '橙某人',
age: 18,
}
let nam1: Name = '橙某人'; // ✅
let nam2: Name = '橙某人1111'; // ❌
let age1: Age = 18; // ✅
let age2: Age = 19; // ❌
typeof
在 JS
中,typeof
能帮我们获取八种数据类型。
javascript
console.log(typeof 1); // number
console.log(typeof '1'); // string
console.log(typeof true); // boolean
console.log(typeof null); // object
console.log(typeof undefined); // undefined
console.log(typeof {}); // object
console.log(typeof Symbol()); // symbol
console.log(typeof 1n); // bigint
而在 TS
中,typeof
能帮我们获取值的类型。
javascript
let name = '橙某人';
let age = 18;
let obj = {
name: '橙某人',
age: 18,
};
type Name = typeof name;
type Age = typeof age;
type Obj = typeof obj;
等价于
javascript
type Name = string;
type Age = number;
type Obj = {
name: string;
age: number;
};
typeof
经常被我们用来获取对象的类型,因为对象属性一般比较多,或者是嵌套对象,这样让定义对象类型成了一个繁琐的事情,使用 typeof
能很好的解决这个问题。
Interface
interface 中文翻译为 "接口",它是声明对象类型的另一种方式。
javascript
interface Obj {
name: string; // 也可以是逗号分隔
age: number;
}
let obj: Obj = {
name: '橙某人',
age: 18
}
let name: Obj.name = '橙某人';
type Age = Obj.age;
可选参数
javascript
interface Obj {
name: string;
age?: number
}
let obj: Obj = {
name: '橙某人'
}
对象方法
javascript
interface Obj {
fn: (a: number) => number;
}
interface Obj {
fn(a: number): number
}
interface Obj {
fn: { (a: number): number };
}
以上三种都是等价的,小编推荐使用第一种。😗
函数
我们知道函数也属性对象的一样,所以 interface
也可以用来声明独立的函数。
javascript
interface Fn {
(a: number, b: number): number
}
const fn: Fn = (a, b) => a + b;
继承
继承是接口的一个最重要特性,interface
可以通过 extends
关键字去继承其他类型,包括 interface
、type
、class
等都可以被继承。
继承 interface
:
javascript
interface Person {
name: string;
}
interface Student extends Person {
age: number;
}
let s1: Student = {
name: '橙某人',
age: 18,
}
继承 type
:
javascript
type Person = {
name: string;
}
interface Student extends Person {
age: number;
}
let s1: Student = {
name: '橙某人',
age: 18,
}
继承 class
:
javascript
class Person {
name: string = '';
say() {
}
}
interface Student extends Person {
age: number;
}
let s1: Student = {
name: '橙某人',
age: 18,
say: () => {}
}
能正常继承 class
的 public
属性和方法,其他修饰符的属性和方法无法继承,或者说是没有意义。
interface
还允许多重继承,可以是继承多个 interface
或者是混合继承。
javascript
interface Person1 {}
interface Person2 {}
type Person3 = {}
class Person4 {}
interface Student extends Person1, Person2, Person3, Person4 {}
但是需要注意❗父类型不能有同名的属性。
重名合并
多个重名的 interface
会进行合并。
javascript
interface Obj {
name: string
}
interface Obj {
age: number;
}
let obj: Obj = {
name: '橙某人',
age: 18
}
- 重名合并时,同一个属性类型不能冲突。
- 重名合并时,同一个属性的函数会发生函数重载。
两者区别
-
继承:
interface
使用extends
关键字能实现继承,但type
只能变向通过&
符号来实现继承。 -
范围:
type
能规范基本类型,联合类型、元组,但interface
不行。 -
合并:
interface
可以重复进行定义,会自动进行合并,但type
不行。 -
typeof:
type
能利用typeof
获取实例类型,进行type
定义,interface
不行。
🍊关于如何获取类型别名或者接口中的某个子类型?
可以使用 []
方括号。
typescript
interface Person {
name: string;
age: string;
sex: string;
}
type P = Person['name']; // string
let p: Person['name']; // string
联合类型与交叉类型
联合类型与交叉类型就像 JS
中的"或"与"且"一样,它们由两个或者多个其他类型组成的类型。
联合类型:|
交叉类型:&
或:||
且:&&
联合类型
联合类型(union types)表示可能是其中任何一种类型的值。
javascript
let articleName: string | number;
// 类型与值类型混合
let age: number | '男' | '女';
// 类型别名
type Person1 = {
name: string;
}
type Person2 = {
age: number;
}
type Person = Person1 | Person2;
// 接口
interface P1 { }
interface P2 { }
interface P {
name: P1 | P2;
age: number | string
}
// 对象
let obj: {
name: string;
age: string | number;
}
// 函数
function fn(a: string | number): void | number { }
// 数组
let arr: Array<number | string>;
arr = [1, 2];
arr = ['a', 'b'];
let arr1: (number | string)[];
类型缩小/类型守护
联合类型本身其实就是将"类型放大"(type widening),但是这在具体逻辑使用的时候就犯难了。
比如:
javascript
function fn1(a: number) {
a.toFixed // ✅
}
function fn2(a: number | string) {
a.toFixed // ❌
}
遇上这种情况,或者说凡是遇到联合类型的情况,在使用之前,我们都需要先进行"类型缩小"(type narrowing)。
javascript
function fn2(a: number | string) {
if(typeof a === 'number') {
a.toFixed
}
}
类型缩小的方式很多种。
javascript
interface P1 {
name: string;
}
interface P2 {
age: number;
}
function fn(obj: P1 | P2) {
if ('name' in obj) {
console.log(obj.name);
}
}
类型缩小有时也被称为类型守护。
交叉类型
交叉类型(intersection types)表示必须满足其中任何一种类型的值。
上述的联合类型示例,都可将 |
符号换成 &
符号,让其变成交叉类型。
尽管这会让很多类型变得无意义,如:
javascript
let articleName: string & number;
我们无法定义出一个值既是 string
类型,又是 number
类型,为此 articleName
的类型将变成一个 never
类型。
当然,这并不是说交叉类型就没有用了,它的主要用处还是在对象类型上。
type
利用交叉类型进行合并行为:
javascript
type P1 = {
name: string;
}
type P2 = {
age: number;
}
let obj: P1 & P2 = {
name: '橙某人',
age: 18
}
不过,如果遇上同名属性,它的合并行为就和 interface
的自动合并行为有点不一样了, interface
是覆盖,而交叉类型则是取交叉部分。
javascript
type P1 = {
name: string;
}
type P2 = {
name: string | number;
}
let obj1: P1 & P2 = {
name: '橙某人', // ✅
}
let obj2: P1 & P2 = {
name: 1, // ❌ name 只能是交叉的 string类型
}
类型断言
类型断言 这玩意又是啥?😮
就一句话,它能帮我们强制通知 TS
"xx 它就是这个类型了,不需要你再帮我判定类型了,给我走"。
类型断言有两种语法形式:value as Type
或者 <Type>value
。
as
我们用上面"联合类型-类型缩小"的例子来看看。
javascript
function fn2(a: number | string) {
(a as number).toFixed
}
我们可以直接将 a
断言成一个 number
类型,那么它就能直接调用 toFixed
方法。
是不是很简单😋
<>
改成尖括号语法,如下:
javascript
function fn2(a: number | string) {
(<number>a).toFixed
}
这也没什么好说的,用就完事了,因为很多时候,我们可能拥有 TS
不能知道的值的类型信息,我们可以直接手动强制断言它。
多重断言
断言并不能是随意的,要有理有据,不能胡说八道喔。
如上面例子,由于是联合类型,所以我们断言成联合类型中的其中一个类型是比较合理的说法。
但是,如下:
javascript
let str = '橙某人';
let num: number = str as number; // ❌
这情况就不行了,很明确的事实摆着呢,你不能欺骗 TS
,要不它得哭了。
类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
例如,这样子是可以的。
javascript
let str = '橙某人';
let num: String = str as String; // ✅
但是却不能满足我们的需求。👊 不慌,这个时候就轮到多重断言出场了。
javascript
let str = '橙某人';
let num1: number = str as any as number; // ✅
let num2: number = str as unknown as number; // ✅
既然能断言成父类型就简单了,我们可以直接断言成顶层类型再转成子类型。
顶层类型有 any
和 unknown
类型,它们兼容所有子类型。
(断言真有点像在胡说八道,搁这骗 TS
呢。😜)
as const断言
as const
断言可以用来修改类型推断的行为,但它只能用在字面量、枚举身上,不能用于变量。
当我们使用类型推断的时候,let
和 const
的行为是不一样的。
javascript
let str = '橙某人'; // string
const Str = '橙某人'; // '橙某人'
let
定义的变量会推断出 string
类型。
const
定义的变量则会推断出 橙某人
值类型。
而当我们使用 as const
断言的时候,可以将 let
的类型推断行为修改成和 const
类型推断的行为一样。
javascript
let str = '橙某人' as const; // '橙某人'
// or
let str = <const>'橙某人'; // '橙某人'
使用 as const
去断言 let
声明的变量后,该变量也具备 const
常量的性质,不可被修改。
javascript
let str = '橙某人' as const;
str = 'a'; // ❌
字面量对象
字面量对象整个对象或者单个属性都可以使用 as const
断言,且两者表现行为不一样,整个对象使用会使所有属性全部会变成只读状态。
js
let obj1 = {
name: '橙某人',
age: 18,
}; // obj类型: { name: string, age: number }
let obj2 = {
name: '橙某人' as const,
age: 18,
}; // obj类型: { name: '橙某人', age: number }
let obj3 = {
name: '橙某人',
age: 18,
} as const; // obj类型: { readonly name: '橙某人', readonly age: 18 }
obj3.name = 'a'; // ❌
字面量数组
字面量数组使用 as const
断言后,类型推断会将其推断成一个只读的元组。
javascript
let arr1 = [1, 2, 3]; // number[]
arr1.push(4);
let arr2 = [1, 2, 3] as const; // readonly [1, 2, 3]
arr2.push(4);// ❌
const arr3 = [1, 2, 3]; // number[]
arr3.push(4);
arr3 = []; // ❌
const arr4 = [1, 2, 3] as const; // readonly [1, 2, 3]
arr4.push(4); // ❌
枚举
枚举的成员也可以使用 as const
断言。
javascript
enum Color {
red,
green,
blue,
}
let red = Color.red; // Color
let red1 = Color.red as const; // Color.red
应用
说了半天,那这鸡肋玩意能干嘛呢?😁
呃...小编用了几年 TS
,好像自己也还没用过。。。
网上看了一圈,有个不错的应用,就是利用字面量数组使用 as const
断言后能变成一个元组的特性。
javascript
function add(a: number, b: number) {
return a + b;
}
const nums = [1, 2];
add(...nums); // ❌
const nums = [1, 2] as const;
add(...nums); // ✅
非空断言(!)
非空断言(Non-null assertion operator)是一种类型断言的特殊形式,用于告诉编辑器一个变量不为 null
或 undefined
。
具体什么意思呢,来看例子:
javascript
let nickname: string | undefined | null;
let realname: string;
function fn() {
nickname = '橙某人';
}
fn();
realname = nickname; // ❌
其实我们能很明确知道 nickname
是会有值的,而且刚好是 string
,但是编辑器却不知道,这种时候就可以使用非空断言。
javascript
let nickname: string | undefined | null;
let realname: string;
function fn() {
nickname = '橙某人';
}
fn();
realname = nickname!; // ✅
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。