基本类型&&变量声明
类型定义
typescript中可以如下定义变量:
let a: string = '你好啊'
上述的: string是对a变量的类型定义
在我们并不对变量进行类型定义时,typescript也可以根据变量的类型进行推断
let a = '你好啊'
a = 2 // ❌ 不能将类型“2”分配给类型“string”
由此看出,在初始化时,若没有主动声明变量类型,typescript也会自行推断,若在被赋值时与初始化类型不同,则编译时会报错
数组定义
两种方式:
let arr: number[] = [1,2,3] // 类型[]
let arr: Array<number> = [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类型表示的是那些永不存在的值的类型。一般不会有实际应用,再次不做过多介绍。
any、 Object、 {}三者对比
当定义一个值为对象类型的时候请设置他为object!!!参见上述Object与object
你可能会试图使用Object或{}来表示一个值可以具有任意属性,因为Object是最通用的类型。 然而在这种情况下any是真正想要使用的类型,因为它是最灵活的类型。
比如,有一个Object类型的东西,你将不能够在其上调用toLowerCase()。
越普通意味着更少的利用类型,但是any比较特殊,它是最普通的类型但是允许你在上面做任何事情。 也就是说你可以在上面调用,构造它,访问它的属性等等。 记住,当你使用any时,你会失去大多数TypeScript提供的错误检查和编译器支持。
如果你还是决定使用Object和{},你应该选择{}。 虽说它们基本一样,但是从技术角度上来讲{}在一些深奥的情况里比Object更普通。
// 这两种都是对的
const a:Object = 1
const a: {} = 1
// 所以单纯设置一个值为对象,使用object
const a: object = {} //对的
const a: object = 1 // ❌
Function、function、() => void
() => void只是简单列举一种,最普通的函数形式,其余的带参数、有返回值的形式,可自行编写,原理与此相同
- typescript中不存在function该种类型的变量类型
- Function只能定义函数类型,不能想Object可以给任何值定义类型(number、string等等),他只是函数的类型定义,因为他不是原型链的最底层,故而不向Object那么通用
- 在Function和() => void之间,最好选用() => void,因为表示的形式更具体,Function表示所有可能的函数类型,() => void只表示符合该项规定的类型,() => void是Function子集
类型断言
一个变量可能有多种变量类型,有时可能需要当变量为一种类型时执行一种操作,为另一种类型时,执行另一个操作,这时就需要用到类型断言,通过类型断言,我们可以准确的告诉编译器我们想要做什么,这个动作仅在编译时起作用。
类型断言两种方式:尖括号和as关键字
type Str = string | number;
let str: Str = '你好啊';
let strLen = (<string>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 <T>(arg: T): T { // hello <T>:定义的泛型变量,arg: T传入参数类型,(arg: T): T返回值类型
return arg;
};
// ES6箭头函数形式(泛型变量只有一个时,eg:T) 正确
const hello = <T extends Object>(arg: T): T => {
return arg;
};
// ES6箭头函数形式(泛型变量只有一个时,eg:T) 错误
const hello = <T>(arg: T): T => {
return arg; // JSX element 'T' has no corresponding closing tag.ts
};
// ES6箭头函数形式(泛型变量两个及已上时) 错误
const hello = <T, U>(arg: T): T => {
return arg; // JSX element 'T' has no corresponding closing tag.ts
};
// 使用
let str = hello<string>('hello');
ES6箭头函数使用泛型,且泛型变量只有一个时,必须使用<T extends Object>形式,原因:TypeScript不确定它是否可能是JSX开始标记。 它必须选择一个,所以它与JSX一起使用。如果你想要一个具有完全相同语义的函数,可以明确列出T的约束,这打破了TypeScript的歧义,以便您可以使用泛型类型参数。 它也具有相同的语义,因为类型参数始终具有{}的隐式约束。
泛型变量
创建上述的泛型函数,编译器要求在函数体中正确使用类型,换句话说,你必须吧这些参数当成时任意类型。比如上述函数中想要console.log(arg.length)就会立即报错,因为存在一些类型并没有length属性,这时可以优化上面的函数
function hello <T>(args: T[]): T[] { // hello <T>:定义的泛型变量,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[],
[propName: string]: any;
}
let type : Type;
type.a = 1;
type.b = 'hello';
type.c = ['a','b'] // ❌ 报错 不能将类型“string”分配给类型“number”
interface BCross {
a: number,
c: string
}
interface BCross {
c: number, // 后续属性声明必须属于同一类型。属性“c”的类型必须为“string”,但此处却为类型“number”。
d: number,
}
const a: BCross = {
a: 1,
c: '3',
d: 3
}
两次声明同一接口,接口中若同一个变量为不同的类型,那么最终,这个变量的类型为初次定义这个变量的类型
交叉类型与联合类型
交叉类型 &(A& B为一个值)
指将多个字典类型合并为一个新的字典类型,既为...又为...(必须全部包含&左右两测的所有信息,不能多也不能少)
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值,必须全部包括
let tt: Type = {a: '12', b: 1, c: 1, d:2}; // ❌对象文字可以只指定已知属性,并且“d”不在类型“Type”中
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,他们的类型必须相同,否则,以此交叉类型为类型的变量会报错
- A、B中若存在两个相同的变量名,则两个相同变量执行&操作:A.a&B.a
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)
联合类型 |(A | 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<T> = {
age: T;
}
// 类型别名在属性里引用自己
type Person<T> = {
age: T;
mother: Person<T>
father: Person<T>
}
字面量类型
字面量类型通常结合联合类型使用
// 最简单的字面量类型
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校验就会报错
- 上面说的父子集关系,都是指对象中没有可选参数的情况,如果有可选参数,那么去除可选参数后,在进行关系比较,确定能否赋值(ps不懂的可以转换思考,可选类型为:类型|undefined)
/*
interface AObj {
a: number;
b: number;
c: number;
}
interface BObj {
a: number;
}
interface CObj {
a: number;
d: string
}
interface DObj {
a: number;
b: string
}
interface EObj {
a: number;
d?: string
}
*/
// variableObj1为父集,variableObj2为子集
let variableObj1: AObj = {
a: 1,
b: 1,
c: 1
};
let variableObj2: BObj = {
a: 2,
};
variableObj2 = variableObj1;
// variableObj1 = variableObj2
// “=”右侧的变量的值不完全包含左侧变量的值,不包含的部分 为 可选项
let variableObj3: EObj = {
a: 3,
d: 'd'
}
variableObj3 = variableObj1;
// “=”右侧的变量的值不完全包含左侧变量的值,不包含的部分 不为 可选项
let variableObj4: CObj = {
a: 4,
d: 'd'
}
let variableObj5: DObj = {
a: 5,
b: 'b'
}
variableObj4 = variableObj1;// ❌Property 'd' is missing in type 'AObj' but required in type 'CObj'
variableObj5 = variableObj1; // ❌不能将类型“AObj”分配给类型“DObj”。属性“b”的类型不兼容。
console.log(variableObj1, variableObj2, variableObj3, variableObj4, variableObj5)
注意:上述所有说的报错,仅指ts校验报错,而并非不能使用,即使ts校验报错,也可以赋值成功,与javascript特性有关(弱类型语言)
函数赋值
在判断两个函数是否能够赋值时,TypeScript对比的是函数签名(输入参数类型、输出的数值类型),输入参数的名字、输出的值,是否相同无所谓,只看参数类型
eg:A = B,将B赋值给A,要看B里的每个参数是否能在A中找到相对应的参数,并且位置从第一个参数开始就类型对应,即A的属性个数>=B,从属性上来讲,A包含B,A⊃B ,参照下图
可以将函数中的参数转变为可以理解的对象形式,下面按照参数的每一位顺序,做对应的对象转换,帮助更容易理解函数赋值,已上图为例,函数参数类型,按照顺序依次映射到对象的a-z字母中去
上述说的【变量 = 变量】比较变量类型,父集关系都是指【非可选变量】,当有可选变量时,比较前去除可选变量在进行变量值比较
// 输出类型不同
let fun1 = () => 0;
let fun2 = () => '1';
fun1 = fun2 // ❌不能将类型“() => string”分配给类型“() => number”。
// 输出值不同
let fun3 = () => 0;
let fun4 = () => 1;
fun3 = fun4
// 输入类型比较
// 输入参数名不同,类型相同
let fun5 = (a: number) => 0;
let fun6 = (b: number) => 0;
fun5 = fun6;
// A包含B,无可选参数情况
let fun7 = (a: number, b: number) => 0
let fun8 = (a: number) => 0
fun7 = fun8;
fun8 = fun7 // ❌不能将类型“(a: number, b: number) => number”分配给类型“(a: number) => number”。
// A包含B,无可选参数情况,但是B中的参数位置,与A中参数的位值,不能做到从第一位开始的位置映射
let fun9 = (b: number, c: string) => 0;
let fun10 = (d: string) => 0;
fun9 = fun10; // ❌ 不能将类型“(d: string) => number”分配给类型“(b: number, c: string) => number”
// A不完全包含B,有可选参数情况
let fun11 = (a: number, b: number) => 0
let fun12 = (a: number, b: number, c?: string) => 0
fun11 = fun12
// A不完全包含B,有可选参数情况,B的可选参数类型与A不能做到位置映射
let fun13 = (a: number, b: number, c: number) => 0
let fun14 = (a: number, b?: string) => 0
fun13 = fun14 // ❌不能将类型“(a: number, b?: string | undefined) => number”分配给类型“(a: number, b: number, c: number) => number”。
参上,可赋值形式为fun(A) = fun(B),要保证以下条件:
- 返回值【类型】相同
- 输入参数【类型】转变为interface后,去除A,B末尾的可选参数
- A⊇B
- B中参数位置与A中参数位置,从第一位开始,一对一映射关系(相同类型)