类型

基本类型&&变量声明

类型定义

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为一个值)

指将多个字典类型合并为一个新的字典类型,既为...又为...(必须全部包含&左右两测的所有信息,不能多也不能少)


image.png
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为一个集合)

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


image.png
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

对象类型赋值

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


    变量 = 变量.png

上述说的【变量 = 变量】比较变量类型,父集关系都是指【非可选变量】,当有可选变量时,比较前去除可选变量在进行变量值比较

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

// 对象赋值
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对比的是函数签名(输入参数类型、输出的数值类型),输入参数的名字、输出的值,是否相同无所谓,只看参数类型

image.png

eg:A = B,将B赋值给A,要看B里的每个参数是否能在A中找到相对应的参数,并且位置从第一个参数开始就类型对应,即A的属性个数>=B,从属性上来讲,A包含B,A⊃B ,参照下图

函数赋值.png

可以将函数中的参数转变为可以理解的对象形式,下面按照参数的每一位顺序,做对应的对象转换,帮助更容易理解函数赋值,已上图为例,函数参数类型,按照顺序依次映射到对象的a-z字母中去

A参数.png

B参数.png

上述说的【变量 = 变量】比较变量类型,父集关系都是指【非可选变量】,当有可选变量时,比较前去除可选变量在进行变量值比较

// 输出类型不同
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中参数位置,从第一位开始,一对一映射关系(相同类型)
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,258评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,335评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,225评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,126评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,140评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,098评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,018评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,857评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,298评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,518评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,400评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,993评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,638评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,661评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352

推荐阅读更多精彩内容