类型

基本类型&&变量声明

类型定义

typescript中可以如下定义变量:

let a: string = '你好啊'

上述的: string是对a变量的类型定义

在我们并不对变量进行类型定义时,typescript也可以根据变量的类型进行推断

let a = '你好啊'
a = 2  // ❌ 不能将类型“2”分配给类型“string”

由此看出,在初始化时,若没有主动声明变量类型,typescript也会自行推断,若在被赋值时与初始化类型不同,则编译时会报错

数组定义

两种方式:

let arr: number[] = [1,2,3]  // 类型[]
let arr: Array = [1,2,3]    // Array<类型>

元组定义

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同,

// 下面定义一个类型数组长度为3,类型分别为string,number,object的数组
interface Type {
 a: number;
 b: string;
}
let x: [string, number, Type];  // x为长度为3的数组
x = ['hello', 735, { a: 23, b: '你好'}]
x[3] = '23';// 不能将类型“"23"”分配给类型“undefined”。Tuple type '[string, number, Type]' of length '3' has no element at index '3'

any

在编程阶段有可能存在还不清楚变量类型的情况,这种情况下,我们不希望类型检查器对这些值进行检查,而是直接让他们通过编译检查,就用到了any,在变量赋值时,any与Object相似,下文会讲到

// 下面代码不会出现类型错误的提示
let a: any = 4;
a = "你好啊";
a = true; 

Object与object

在javascript中万物皆对象Object,因此,在赋值方向,Object与any有这类似的作用,Object类型的变量允许给它赋任意值,但与any相比,它不能在上面调用任意方法如下:

let A: any = 4;
A = '123';
A.ifItExists(); // 实际上在A上没有ifItExists函数,但是因为跳过编译检查,所以不会检查这一步,因此不会报错
A.toFixed();

let B: Object = 4;
B = '123';    // javascript中,万物皆对象
B.toFixed(); // ❌类型“Object”上不存在属性“toFixed”

let C: object = 3;  // ❌不能将类型“3”分配给类型“object”
C = '123';    // object单纯的指类型为对象

let D: {} = 2;    // {} 与Object作用相同
D = '13';
D.toFixed();  // ❌类型“{}”上不存在属性“toFixed”

这里面any和Object的区别就在于,any是跳过编译检查,既然已经跳过了,那么在对这个变量做任何处理都不会报错,Object类型的变量由number被赋值成了字符串成功,因为javascript中,万物皆对象,都从对象继承过来的,所以赋值会成功,但是toFixed()函数在Object中不存在,因此不能直接调用,{}作用与Object相同。Object的所有效果在{}都能表现出来。
object则单纯指类型为对象,表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void,用void声明一个变量一般没有什么意义,因为他只能赋值undefined,一般void用来声明函数的返回值,当没有返回值时,使用void

let A: void = undefined;  
let B: void = null;  // ❌不能将类型“null”分配给类型“void”
function fun1(): void {
  console.log('你好啊');
}

Never

never类型表示的是那些永不存在的值的类型。一般不会有实际应用,再次不做过多介绍。

类型断言

一个变量可能有多种变量类型,有时可能需要当变量为一种类型时执行一种操作,为另一种类型时,执行另一个操作,这时就需要用到类型断言,通过类型断言,我们可以准确的告诉编译器我们想要做什么,这个动作仅在编译时起作用。

类型断言两种方式:尖括号和as关键字

type Str = string | number;
let str: Str = '你好啊';
let strLen = (str).length    // <类型>变量
type Str = string | number;
let str: Str = '你好啊';
let strLen = (str as string).length    // 变量 as 类型

若当前的类型并不等于断言的类型,则该条语句不被执行

type Str = string | number;
let str: Str = 2;
let strLen = (str as string).length
console.log('strLen', strLen)  // undefined

在JSX文件中TypeScript的类型断言可以使用as,不允许使用尖括号方式

泛型

目的:用于提升代码的重用性

泛型函数

泛型函数的定义与使用

// 普通函数形式
function hello (arg: T): T {    // hello :定义的泛型变量,arg: T传入参数类型,(arg: T): T返回值类型
    return arg;
};
// ES6箭头函数形式(泛型变量只有一个时,eg:T)    正确
const hello = (arg: T): T => {
    return arg;
};
// ES6箭头函数形式(泛型变量只有一个时,eg:T)    错误
const hello = (arg: T): T => {
    return arg;    // JSX element 'T' has no corresponding closing tag.ts
};

// ES6箭头函数形式(泛型变量两个及已上时)    错误
const hello = (arg: T): T => {
    return arg;    // JSX element 'T' has no corresponding closing tag.ts
};

// 使用
let str = hello('hello');

ES6箭头函数使用泛型,且泛型变量只有一个时,必须使用形式,原因:TypeScript不确定它是否可能是JSX开始标记。 它必须选择一个,所以它与JSX一起使用。如果你想要一个具有完全相同语义的函数,可以明确列出T的约束,这打破了TypeScript的歧义,以便您可以使用泛型类型参数。 它也具有相同的语义,因为类型参数始终具有{}的隐式约束。

泛型变量

创建上述的泛型函数,编译器要求在函数体中正确使用类型,换句话说,你必须吧这些参数当成时任意类型。比如上述函数中想要console.log(arg.length)就会立即报错,因为存在一些类型并没有length属性,这时可以优化上面的函数

function hello (args: T[]): T[] {    // hello :定义的泛型变量,arg: T传入参数类型,(arg: T): T返回值类型
    console.log(args.length);
    return arg;
};

这样泛型变量T就作为了数组的一部分属性,而不是作为整体类型,增加了灵活性,此时传入的参数会发生变化,由eg:string -->string[];

枚举

TypeScript支持数字枚举和字符串枚举

数字枚举

数字枚举,后面的枚举变量时递增的,第一个枚举变量默认值为0,后续依次递增,若重新定义了第一个枚举变量,则后续的枚举变量在已定义的变量之上递增,
存在反向映射,可以通过值value拿到命名的key

enum Type {
  a,    // 0
  b,    // 1
  c = 8,    // 8
  d    // 9
}

反向映射:

const value = Type.a;    // 0
const key = Type[0];    // a

字符串枚举

字符串枚举没有递增的含义,每个枚举成员必须手动初始化,不存在反向映射

enum Type {
  a = 'a',   // a
  b = 'b',    // b
  c = 'c',    // c
}

可以存在数字枚举与字符串枚举共存的情况,但是前提是字符串枚举必须放在下面

enum Type {
  a,
  b,
  c=8,
  d='ddd',
  e='eee'
}

enum 类型的变量不能在.d.ts文件中导出,会报Cannot find module './data'错误

高级类型

interface

描述对象的结构,对字典(数据结构)进行类型约束

interface Type {
a: number,
b: string,
c: number[]
}
let type : Type;
type.a = 1;
type.b = 'hello';
type.c = ['a','b']    // ❌ 报错       不能将类型“string”分配给类型“number”

交叉类型与联合类型

交叉类型 &

指将多个字典类型合并为一个新的字典类型,既为...又为...

interface A {
  a: string,
  b: number
}
interface B {
  b: number,
  c: number
}
type Type = A & B;

let t: Type = {a: '12', b: 1, c: 1};    // t变量必须包含a、b、c三个key值

一般交叉类型只适用于对象,举个例子,

type Type = number & string;
let t : Type = 1    // ❌ 不能将类型“1”分配给类型“never”

因为number和string类型没有交集的情况。所以number & string后不会有能包含两种的类型的值存在。所以上文说一般交叉类型只适用于对象
还存在另一种情况,上面代码中的A、B中都有相同的b,他们的类型必须相同,否则,以此交叉类型为类型的变量会报错

interface A {
  a: string,
  b: number
}
interface B {
  b: string,
  c: number
}
type Type = A & B;

let t: Type = {a: '12', b: 1, c: 1};    // ❌ 不能将类型“number”分配给类型“never”

代码中的key值b,会进行类似下面的操作:b:number & string;因此这样的类型还是不存在,因此代码中会报错

总结一下:交叉类型就是将不同类型叠加成为新的类型,并且包含了所有类型(除非里面的类型为可选的eg:b?:string,这样可以不用写b)

联合类型 |

表示一个变量可以是几种类型之一(或的关系),可以是...也可以是...

type Type = number | string;
let t: Type = 1;

类型保护

要实现类型保护,只需要简单地定义一个函数就可以,但返回值必须是一个主谓宾语句

function isTeacher(person: Teacher | Student): person is Teacher {
  return (person as Teacher).teach !== undefined;
}

person is Teacher是类型保护语句,说明参数必须来自于当前函数签名(定义了函数或方法的输入输出)里的一个参数名

类型别名 type

使用type关键字来描述类型变量,使用类型别名或者类型集合创建一个新名字,类型别名可以是泛型,也可以是用类型别名在属性里引用自己,听起来比较像递归

// 普通
type Age = number;
// 泛型
type Person = {
  age: T;
}
// 类型别名在属性里引用自己
type Person = {
  age: T;
  mother: Person 
  father: Person 
}

字面量类型

字面量类型通常结合联合类型使用

// 最简单的字面量类型
type Profession = 'teacher';
// 结合联合类型
type Profession = 'teacher' | 'doctor' | 'student';
let person: Profession    // person的值在 teacher、 doctor、student中

类型推导

在没有明确指出类型的地方,TypeScript编译器会自己推测出当前变量的类型,TypeScript里的类型兼容性是基于结构子类型的,只要满足了子类型的描述,那么就可以通过编译时检查,TypeScript的设计思想比不是满足正确的类型,而是满足能正确通过编译的类型,这就造成了运行时和编译时可能存在类型偏差。以下类型是可以通过编译的:

interface Person {    // Person相当于A
  age: number;
}
class Father {      // Father相当于B
  age: number;
  name: string;
}
let person: Person;
person = new Father();

也就是说TypeScript结构化类型系统的基本规则如下:如果A想要兼容y,那么B至少具有与x相同的属性,
eg: A = B,将B赋值给A,要看A里的每个参数是否能在B中找到相对应的参数,即A的属性个数<=B,从属性上来讲,B包含A,A⊂B

原始类型和对象类型赋值

  • 变量 = 值:遵循值类型与变量定义的类型相同原则
  • 变量 = 变量:遵循变量定义的类型与变量定义的类型比较原则,详情参见下方

拿一个赋值的例子解释一下赋值时的过程

// 对象赋值
interface Person {    // 编译通过
  age: number;
}
let person: Person;
const alice = { name: '123', age: 11}
person = alice;

// 检查函数参数
function Test (person: Person){}    // 编译通过
Test(alice)

alice在赋值给person时,编译器首先查看person中的每个属性,看是否能在alice中找到所有person应该有的属性,在上面中发现person有的属性age,aliceu 也有,因此就判断赋值合理
这套赋值检查的程序,在检查函数参数时同样奏效,编译给通过

解释一下常见的问题,a赋值给b可以,再这个操作之后将b赋值给a就ts报错

interface A {
  m: number;
  n: string;
}
interface B {
  m: number;
}
// 状态一
let a: A = { m: 1, n:'1' };
let b: B = { m: 2 };
b = a;
a = b;  // Property 'n' is missing in type 'B' but required in type 'A'.

// 状态二
let a = { m: 1, n:'1' };
let b: B = { m: 2 };
b = a;
a = b;  // Property 'n' is missing in type 'B' but required in type 'A'.

原因是:按照上文解释,b中的的所有属性,a中都有,所以可以将a赋值给b,但是也只是ts判断赋值合理,虽然此刻b的值为{ m: 1,n:'1' },但是实际上他的值的类型还是B类型即interface B { m: number;},这个是定义变量时已经确定了的,即便是值被更改,但是这个变量的类型也不会被更改

interface B {
  m: number;
}

// 状态一
let b: B;
b = { m: 3, n: 2 };  // 不能将类型“{ m: number; n: number; }”分配给类型“B”。对象文字可以只指定已知属性,并且“n”不在类型“B”中。

// 状态二
let b: { m:2 };
b = { m: 3, n: 2 };  // 不能将类型“{ m: number; n: number; }”分配给类型“B”。对象文字可以只指定已知属性,并且“n”不在类型“B”中

上述状态一和状态二的原理其实是一样的,b是被直接赋给了值,而不是付给一个变量,编译器就不会去像之前一样对比a、b两个变量的属性类型,那么他就会直接去对比当前B的类型,发现B类型interface B { m: number; }中并不包含n这个属性,ts校验就会报错

注意:上述所有说的报错,仅指ts校验报错,而并非不能使用,即使ts校验报错,也可以赋值成功,与javascript特性有关(弱类型语言)

函数赋值

在判断两个函数是否能够赋值时,TypeScript对比的是函数签名(输入参数类型、输出的数值类型),输入参数的名字、输出的值,是否相同无所谓,只看参数类型
eg:A = B,将B赋值给A,要看B里的每个参数是否能在A中找到相对应的参数,并且位置从第一个参数开始就类型对应,即A的属性个数>=B,从属性上来讲,A包含B,A⊃B ,参照下图

类型_第1张图片
函数赋值.png

let fun1 = (a: number) => 0;
let fun2 = (b: number, c: string) => 1;
let fun3 = (c: number, d: string) => 0;
let fun4 = (d: string) => 2;
let fun5 = (d: number) => '2';
let fun6 = (b: number, c?: string) => 1;

// 输入参数类型不同
fun1 = fun4;    // ❌ 不能将类型“(d: string) => number”分配给类型“(a: number) => number”。参数“d”和“a” 的类型不兼容。不能将类型“number”分配给类型“string”。
// 输出值类型不同
fun4 = fun5;  // ❌ 不能将类型“(d: number) => string”分配给类型“(d: string) => number”。参数“d”和“d” 的类型不兼容。不能将类型“string”分配给类型“number”。
// A = B,A包含B,但是B中的参数位置,与A中参数的为值,不能做到从第一位开始的位置映射
fun2 = fun4;  // ❌ 不能将类型“(d: string) => number”分配给类型“(b: number, c: string) => number”。参数“d”和“b” 的类型不兼容。不能将类型“number”分配给类型“string”。
// A = B,B的参数多
fun1 = fun2;   // ❌ 不能将类型“(b: number, c: string) => number”分配给类型“(a: number) => number”。
// A = B,B的参数多,类型不一致的参数为可选
fun1 = fun6;
// A = B,A包含B
fun2 = fun1;
// 输入参数名不同,输出参数值不同,类型相同
fun2 = fun3;

参上,可赋值形式为:

  • A = B,B的参数多,B中与A类型不一致的参数为可选
  • A = B,A包含B,并且类型参数位置相同
  • A = B,输入参数名不同,输出参数值不同(满足两个的其中一个就行),类型相同

不可赋值形式为:

  • A = B,函数输入参数类型不同
  • A = B,函数输出值类型不同
  • A = B,A包含B,但是B中的参数位置,与A中参数的为值,不能做到从第一位开始的位置映射
  • A = B,B的输入参数比A的多,或者A不能包含B即B中有部分参数不在A中

你可能感兴趣的:(类型)