TypeScript入门
TypeScript 是一种由微软开发的自由和开源的编程语言。它是 JavaScript 的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程。
TypeScript 提供最新的和不断发展的 JavaScript 特性,包括那些来自 2015 年的 ECMAScript 和未来的提案中的特性,比如异步功能和 Decorators,以帮助建立健壮的组件。下图显示了 TypeScript 与 ES5、ES2015 和 ES2016 之间的关系:
数据类型
js包含的数据类型
Boolean
Number
String
Array
Function
Object
Symbol
undefined
null
TS新增的数据类型
void
any
never
元组
枚举
类型注解
它相当于强类型语言中的类型声明具体语法如:(变量/函数):type
,即在变量名后面加一个“类型”,如:
let n: number = 100
let a:String = "123"
通过添加类型注解之后,我们就不能随便将其他类型的数据赋值给当前变量了,比如上面的 n,我们不能将字符串赋值给它,不然 vscode 将会给我们格式高亮报错Type '"xxx"' is not assignable to type 'number'.
数组
数组在 TS 中的声明方式是这样的:
// 方式一
let arr1: number[] = [1, 2, 3]
// 方式二
let arr2: Array = [1, 2, 3]
// 联合类型
let arr: Array = [1, 'abc']
元组
元组是一种特殊的数组,它控制了数组成员的类型和数量,比如:
let tuple: [number, string] = [1, 'abc']
上面代码就控制了三个条件:
数组第一个元素是数字类型;
数组第二个元素是字符串类型;
数组中只能有两个元素;
元组的越界问题:
let tuple: [number, string] = [1, 'abc']
tuple.push(2)
console.log(tuple) // [1, "abc", 2]
tuple[2] // 编辑器会报错
虽然元组可以通过 push 方法添加新元素,但是元组仍然无法访问刚添加的新元素。
undefined、null
当一个变量被声明成 undefined 或 null,该变量只能被赋值 undefined 或者 null:
let undef: undefined = undefined
let nul: null = null
在 ts 官网中,undefined 和 null 是任何类型的子类型,说明 undefined 和 null 可以赋值给任何其他类型,但不能直接赋值:
/* 会报错 */
let num: number
num = undefined
num = null
要使得上面代码不报错,需要修改下 ts 的配置文件:
...
"strictNullChecks": false, /* Enable strict null checks. */
...
// 或者使用联合类型
let num: number | undefined | null
num = undefined
void
在 ts 中,void 的意思是“没有返回值”,通常用来标示函数没有返回值(可以理解为函数没有 return)
function test():void {
console.log('啥也没有');
}
test()
any
any 即任何类型,ts 中只要不指定类型注解即可。
never 类型
从来不会出现的值,是其他类型的子类型包括null和undefined.
never的主要作用就是充当Typescript类型系统里的Bottom Type (Typescript还有个top type unknown和即是top也是bottom的any),
any 类型的变量可以被赋值以任意类型的值,而 unknown 则只能接受 unknown 与 any.
const strOrNumOrBool: string | number | boolean = true;
if (typeof strOrNumOrBool === "string") {
console.log("str!");
} else if (typeof strOrNumOrBool === "number") {
console.log("num!");
} else {
const _exhaustiveCheck: never = strOrNumOrBool;
}
//never是确保代码正确性的一种手段
void 与 never:
void 可以理解为一个没有 return 但能执行完毕的函数;
never 是一个函数体无法结束执行的函数;
枚举
使用枚举我们可以定义一些带名字的常量。 使用枚举可以清晰地表达意图或创建一组有区别的用例。 TypeScript支持数字的和基于字符串的枚举。枚举使用 enum
关键字来定义。
enum OrderStatus {
ToBePaid = 30,
Payments = 40,
PaymentCompleted = 50
}
const orderInfo = {status:30,a:'122',b:'222'}
if(orderInfo.status === OrderStatus.ToBePaid || orderInfo.status === OrderStatus.PaymentCompleted) {
console.log('1111');
}
类型断言
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";
let strLength: number = (someValue).length;
另一个为as
语法:
let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;
注意:A 数据类型的变量 断言为 B数据类型的变量的时候 A数据类型和B数据类型必须有重叠关系.
重叠关系的理解:
函数
函数的声明
js中函数声明两种常用的方式
function test() {
console.log("test");
}
test()
let test1 = function() {
console.log("test1");
}
test1()
ts中函数的声明
function test():void {
console.log("test");
}
test()
let test1 = function():void {
console.log("test1");
}
test1()
可选参数
function getInfo(name:string, age:number):string {
return `我叫${name},年龄${age}岁`
}
// 在ts中这么写编译器会报错
//未提供 "age" 的自变量。
getInfo('胡杨')
// 如果第二个参数可以不传的话,需要配置可选参数,写法如下
// 在参数后面加'?',使其变为可选参数
// 注意:可选参数必须配置在参数最后面
function getInfo(name:string, age?:number):string {
return `我叫${name},年龄${age}岁`
}
getInfo('胡杨')
默认参数
// 上述例子也可以通过配置参数默认值来解决
function getInfo(name:string, age:number=18):string {
return `我叫${name},年龄${age}岁`
}
getInfo('胡杨')
剩余参数
function add(...result:number[]):number{
let sumNum:number = 0;
result.map(item => {
sumNum += item
})
return sumNum
}
const sum = add(1,2,3)
console.log(sum);
方法重载
// 声明部分
function getInfo(name:string):void;
function getInfo(age:number):void;
// 实现部分
function getInfo(value:any):void {
if(typeof value === 'string') {
console.log("我的名字是:",value);
} else {
console.log("我的年龄是:",value);
}
}
getInfo('胡杨') // 我的名字是: 胡杨
getInfo(18) // 我的年龄是: 18
实现签名参数个数可以少于重载签名参数个数(但少的这个参数不能被使用)
重载签名必须提供返回值类型,TS无法自动推导
实现签名可以不提供返回值类型,TS可以推导出来
类
类的定义
class Person {
name:string;
constructor(name:string) {
this.name = name
}
work() {
console.log(this.name + '写bug');
}
}
const p = new Person('张三')
p.work() //张三写bug
类里面的修饰符
public:公有,在类内部,子类和类的外部都可以访问;属性不加修饰符,默认的是public.
protected:保护类型,在类的内部和子类可以访问,在类的外部不可以访问
private:私有,在类的内部可以访问,在子类和类的外部都不可以访问
-
readonly
readonly修饰符
你可以使用
readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Octopus {
readonly name: string;
readonly numberOfLegs: number = 8;
constructor (theName: string) {
this.name = theName;
}
}
let dad = new Octopus("Tom");
dad.name = "Jery"; // 错误! name 是只读的.
参数属性
参数属性可以方便地让我们在一个地方定义并初始化一个成员。
class Octopus {
// name 前面必须有属性修饰符 private public protected readonly
constructor(public name: string) {
}
}
//等价于
class Octopus {
public name:string;
constructor(name: string) {
this.name = name;
}
}
get和set方法
想在赋值或者取值的时候做一些操作.
class Employee {
private _fullName: string; // 参数前面必须加'_'
constructor(name:string){
this._fullName = name;
}
get fullName(): string {
return this._fullName;
}
set fullName(value: string) {
this._fullName = value + '处理标记';
}
}
let employee:Employee = new Employee('Tom');
employee.fullName = 'Jery';
console.log(employee.fullName);
静态属性和静态方法
class Person {
static nick:string;
static run() {
console.log(this.nick + '跑步');
}
}
Person.nick = '老张'
Person.run()//老张跑步
类的继承
class Person {
static nick:string;
name:string;
constructor(name:string) {
this.name = name;
}
static run():void{
console.log(this.nick + '跑步');
}
work():void {
console.log(this.name + '写bug');
}
}
class Student extends Person {
constructor(name:string) {
super(name)
}
}
//报错,静态方法和静态属性不会被继承
// Student.nick = '小八'
// Student.run()
const stu = new Student('小八')
stu.work() //小六写bug
构造器重载
类的构造器也是支持重载,写法与方法的重载是一致的.
interface IBox {
x : number;
y : number;
height : number;
width : number;
}
class Box {
public x: number;
public y: number;
public height: number;
public width: number;
constructor();
constructor(obj: IBox);
constructor(obj?: any) {
this.x = obj && obj.x || 0
this.y = obj && obj.y || 0
this.height = obj && obj.height || 0
this.width = obj && obj.width || 0;
}
}
抽象类
TS中的抽象类是提供给其他类继承使用的,不能直接实例化.
使用abstract关键字定义抽象类和抽象方法,抽象类中的抽象方法不包含具体实现并且必须在派生类中实现.
abstract抽象方法必须写在抽象类里面,抽象类和抽象方法是用来制定标准的
//抽象类demo
abstract class Animal {
abstract eat();
}
//不允许直接创建
const a = new Animal() //无法创建抽象类的实例。
class Dog extends Animal {
eat() { // 必须实现抽象类内部的抽象方法
console.log('吃食物');
}
}
抽象类的应用
// 使用抽象类来适配接口,适配器模式的一种使用
interface VehicleProtocol {
unlock():void;
lock():void;
light():void;
run():void;
shengLang():void;//声浪
}
abstract class S60Adapter implements VehicleProtocol {
abstract unlock(): void;
abstract lock(): void;
light(): void {
throw new Error("Method not implemented.");
}
abstract run(): void;
shengLang(): void {
throw new Error("Method not implemented.");
}
}
class S60 extends S60Adapter {
unlock(): void {
throw new Error("Method not implemented.");
}
lock(): void {
throw new Error("Method not implemented.");
}
run(): void {
throw new Error("Method not implemented.");
}
}
接口
在面向对象的编程中,接口是一种规范的定义,它定义了行为和动作的规范.
接口的种类
属性接口
函数接口
可索引接口
类接口
接口扩展
属性接口
一般是对传入对象的约束
// 接口的写法
interface fullName {
firstName:string; //必须;结尾
secondName:string
}
function getName(name:fullName):string {
return name.firstName + name.secondName
}
// 错误
const nameStr = getName({firstName:'胡'})
const nameStr = getName({firstName:'胡',secondName:'杨',age:20})
const nameStr = getName('胡杨')
// 正确写法
const nameStr = getName({firstName:'胡',secondName:'杨'})
console.log(nameStr); // 胡杨
接口的可选属性
interface fullName {
firstName?:string;
secondName:string
}
function getName(name:fullName):string {
return name.firstName + name.secondName
}
const nameStr = getName({secondName:'杨'})
console.log(nameStr);
函数类接口
对方法传入的参数以及返回值进行约束(批量约束)
interface track {
(key:string, prams:object):boolean;
}
// 函数的必须和接口定义的函数类型一致
const trackEvent:track = function(key:string,prams:object):boolean {
console.log(key + JSON.stringify(prams));
return true
}
const result = trackEvent('appleOpen',{activityId:123,id:12345})
console.log(result);
可索引类型接口
针对数组、对象的约束(不常用)
//普通的数组写法
let arr:number[] = [1,2,3]
let arr2:Array = [2,3,4]
//接口写法
interface MyArray {
[index:number]:string
}
let arr3:MyArray = ['123','234']
console.log(arr3[0]);
interface MyObj {
[index:string]:string
}
let obj:MyObj = {key:'123',value:'234'}
console.log(obj.key);
类接口
对类的约束,和抽象类有点类似
interface Animal {
name:string;
eat(food:string):void;
}
class Dog implements Animal {
name: string;
constructor(name:string) {
this.name = name
}
eat(food: string): void {
console.log(this.name + "吃" + food);
}
}
let d:Dog = new Dog('小富贵儿')
d.eat('狗粮')
接口扩展
接口的扩展也就是对接口的继承
interface Animal {
name:string;
eat(food:string):void;
}
interface Person extends Animal {
work():void;
}
class Man implements Person {
name: string;
constructor(name:string) {
this.name = name
}
eat(food: string): void {
console.log(this.name + '吃' + food);
}
work(): void {
console.log(this.name + '打代码');
}
}
let m:Man = new Man('老八')
m.eat('肉')
m.work()
泛型
我们在软件工程中不仅要创建一致且定义良好的API,同时也要考虑可重用性.组件不仅要支持现在的数据类型同时也要支持未来的数据类型.
通俗来讲,泛型就是解决类、接口和方法的复用性以及对不特定数据类型的支持.
问题:写一个方法,要求这个方法传入的数据类型和传出的数据类型是一致的.
function getData(value:any):any {
return value;
}
// 这种写法可以满足上述的需求,但是它放弃了类型校验,有没有更好的写法呢?
// 泛型写法
function getData(value:T):T {
return value;
}
getData('123')
//虽然any类型或unknown类型能够让identity函数变得通用,使其能够接受任意类型的参数,但是却失去了参数类型与返回值类型相同这个重要信息。
泛型类
若类的定义中带有类型参数,那么它是泛型类。
class MinClass {
// static name?:T; // 错误,类型
//因为类的静态成员是类构造函数类型的一部分,所以泛型类型参数不能用于类的静态成员
public list:T[] = [];
add(value:T):void {
this.list.push(value)
}
sum():T{
let min = this.list[0];
for(let i = 0; i < this.list.length; i ++) {
if(min > this.list[i]) {
min = this.list[i]
}
}
return min;
}
}
let min1 = new MinClass();
min1.add(1);
min1.add(2);
min1.add(3);
console.log(min1.sum());
let min2 = new MinClass();
min2.add('1')
min2.add('2')
min2.add('3')
console.log(min2.sum());
泛型和any的区别
any会丢失类型而泛型不会
泛型的接口
若接口的定义中带有类型参数,那么它是泛型接口。在泛型接口定义中,形式类型参数列表紧随接口名之后。
interface Config {
(value:T):T;
}
let getData:Config = function(value:T):T {
console.log(value);
return value
}
getData(123)
泛型约束
//我们使用泛型变量定义一个函数,但是编译器不知道要传入的value有没有length这个属性,所以这里会给我们报错
function getLength(value:T):T {
console.log(value.length); // 类型“T”上不存在属性“length”
return value;
}
// 那么能不能让getLength在调用的时候传入的一定是带length属性的值呢,比如string类型和array类型
// 答案是可以的,这个时候我们就需要对传入的类型进行约束,写法如下
interface Lengthwise {
length: number;
}
function getLength(value:T):T {
console.log(value.length);
return value;
}
getLength(123); //报错
getLength('123'); // OK
多态
多态: 父类型的引用指向子类型的对象,不同类型的对象针对相同的方法,产生了不同的行为.
class Animal {
name:string
constructor(name:string) {
this.name = name;
}
eat():void {
console.log('吃东西');
}
}
class Person extends Animal {
eat(): void {
console.log(this.name + '吃馒头');
}
}
class Dog extends Animal {
eat(): void {
console.log(this.name +'吃狗粮');
}
}
let p:Animal = new Person('人类');
p.eat();
let d:Animal = new Dog('小狗');
d.eat()
类型守卫
类型守卫是在块级作用域内缩小变量类型的一种类型推断行为.
类型守卫的种类
typeof 的缺点
只能判断 number | string | bigint | bool | symbol | undefine | object | function
typeof null 也是 object
不能判断 Array Map等数据类型,可以使用Object.prototype.toString.call();来代替
不能判断自定义的类, 可以使用 instanceof 来实现
class Person {
name:string;
}
let p:Person = new Person();
console.log(p instanceof Person);
自定义类型守卫
function 函数名(形参:参数类型[大多数为any]) : 形参 is A类型 {
return true or false
}
// 在对复杂的判断进行封装的时候,可以让代码具有很好的复用性
例子:
class Fater {
name:string;
constructor(name:string) {
this.name = name;
}
}
class Son extends Fater {
game(){
console.log('喜欢玩游戏');
}
}
class Daughter extends Fater {
picture(){
console.log('喜欢画画');
}
}
function isSon (son:any) : son is Son {
return son instanceof Son;
}
function getInfo(child:Fater):void {
if(isSon(child)) {
child.game();
}
}
getInfo(new Son('儿子'));
应用场景
鸭子类型是很多面向对象(OOP)语言中的常见做法。它的名字来源于所谓的“鸭子测试”:
当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子,那么这只鸟就可以被称为鸭子.
当有一个滑板车,它有模拟声浪,速度有70km/h,而且还卖两万美金,那么这个滑板车就是S90L.
class Vehicle {
constructor(public name:string) {
}
}
class SomeOneVehicle extends Vehicle{
constructor(public name:string ,public speed:number, public analogSoundWaves:boolean,public price:number) {
super(name)
}
show(): void {
console.log(this.name);
}
}
function isS90(vehicle:any):vehicle is SomeOneVehicle {
return vehicle.speed === 70 && vehicle.analogSoundWaves && vehicle.price === 20000;
}
function getVehicleInfo(vehicle:Vehicle):void {
if(isS90(vehicle)) {
vehicle.show();
}
}
let s90 = new SomeOneVehicle('s90',70,true,20000);
getVehicleInfo(s90);
类型编程
图灵完备
图灵完备证明:https://zhuanlan.zhihu.com/p/408498000
如果一门计算机语言具备内存改变和if/else等控制流的话,这个语言就是图灵完备的.
TS的类型和值都是图灵完备的,TS的类型也是可以运算的,而且类型具备自己单独的内存空间.因为这种运算具有一定的复杂度所以又被戏称为'类型体操';
类型体操
模式匹配:一个类型匹配一个模式类型,提取其中的部分类型到 infer 声明的局部变量中
构造:通过映射类型的语法来构造新的索引类型,构造过程中可以对索引和值做一些修改
递归:当处理数量不确定的类型时,可以每次只处理一个,剩下的递归来做
类型体操相关
infer
// 类型1
type customerFun = (cust: string) => number;
type inferType = T extends (params: infer P) => any ? P : T;
type res = inferType // res:string
// 类型2
type customerFun = (cust: string) => number;
type inferType = T extends (params: any) => infer P ? P : T;
type res = inferType // res:number
// 类型3
type arr = Array
type inferType = T extends Array ? P : T;
type res = inferType // res:number
命名空间
在代码量较大的情况下,为了避免命名冲突,可将相似功能的类、函数、接口等放在命名空间内.
命名空间和模块的区别
命名空间:内部模块,主要用于组织代码,避免命名冲突.
模块:外部模块的简称,侧重代码的复用,一个模块可以包含多个命名空间.
namespace A {
class Animal {
name:string;
constructor(name:string) {
this.name = name;
}
eat(food:string):void {
console.log(this.name + "吃" + food);
}
}
export class Dog extends Animal {
constructor(name:string) {
super(name)
}
play(str:string):void {
console.log(this.name + "玩" + str);
}
}
}
namespace B {
class Animal {
name:string;
constructor(name:string) {
this.name = name;
}
eat(food:string):void {
console.log(this.name + "吃" + food);
}
}
export class Dog extends Animal {
constructor(name:string) {
super(name)
}
play(str:string):void {
console.log(this.name + "不玩" + str);
}
}
}
const d = new A.Dog('富贵儿');
d.play('球');
const d2 = new B.Dog('小花');
d2.play('球');
装饰器
装饰器是一种特殊类型的声明,它能够附加到类,属性,方法和参数上,可以修改类的行为.
通俗来讲,装饰器就是一个方法,可以注入到类,属性,方法和参数上来对他们进行扩展
常见的装饰器有
类装饰器
属性装饰器
方法装饰器
方法参数装饰器
装饰的写法分为
普通装饰器(不可传参)
装饰器工厂(可传参)
类装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
//普通装饰器
function logClass(param:any) {
// 类装饰器 param 返回的是Person构造函数
console.log(param);
param.prototype.age = 18;
param.prototype.run = function(){
console.log('锻炼身体');
}
}
// 重载构造函数
function logClass(target:any) {
return class extends target {
name:string = '老七';
eat():void {
console.log(this.name+':不吃也行');
}
}
}
// 装饰器工厂
function logClass(value:any) {
return function (target:any) {
console.log('value',value);
console.log('target',target);
}
}
@logClass
class Person {
name:string;
constructor(name:string) {
this.name = name
}
eat():void {
console.log('人是铁饭是钢');
}
}
const p:any = new Person('老六')
p.eat();
console.log(`${p.name}:${p.age}`);
p.run();
属性装饰器
属性装饰器表达式会在运行时当做函数被调用,传入下列两个参数:
对于静态成员变量来说是构造函数,对于实例成员变量是类的原型对象
成员的名字
function logProperty(value:any) {
return function (target:any,key:string) {
console.log('value',value);
console.log('target',target);
console.log('attr',key);
target[key] = value;
}
}
class Person {
@logProperty('hello')
name?:string;
eat():void {
console.log(this.name+':吃饭');
}
}
const p:any = new Person()
p.eat();
方法装饰器
用来监视修改或者替代方法定义
传入以下3个参数
对于静态方法来说是构造函数,对于实例方法是类的原型对象
成员的名字
成员的属性描述
function logMethod(value:any) {
return function (target:any,methodName:any,desc:any) {
console.log('value',value);
console.log('target',target);
console.log('methodName',methodName);
console.log('desc',desc);
}
}
class Person {
name:string;
constructor(name:string) {
this.name = name
}
@logMethod('hello')
eat():void {
console.log(this.name+':人是铁饭是钢');
}
}
const p:any = new Person('老六')
demo:把方法传入的参数变成string
function get(value:any) {
return function (target:any,methodName:any,desc:any) {
const oldMethod = desc.value;
desc.value = function (...args:any[]) {
args = args.map(item => {
return String(item)
})
console.log('args',args);
console.log('装饰器里面的方法');
// oldMethod.apply(this,args); 加这句是修改方法,不加就是替换方法
}
}
}
class Person {
@get('1234')
getInfo(...args:any[]):void {
console.log('args',args);
console.log('getInfo里面的方法');
}
}
const p:any = new Person()
p.getInfo('老三',18);
方法参数装饰器
方法参数装饰器在调用的时候会被当做函数调用,传入以下3个参数
对于静态方法来说是构造函数,对于实例方法是类的原型对象
方法的名字
参数在方法参数列表中的索引
function logParams(value:any) {
return function (target:any,paramsName:any,index:any) {
console.log('value',value);
console.log('target',target);
console.log('paramsName',paramsName);
console.log('index',index);
}
}
class Person {
name:string;
constructor(name:string) {
this.name = name
}
eat(@logParams('苹果') food:string):void {
console.log(this.name+':人是铁饭是钢');
}
}
const p:any = new Person('小明');
p.eat('香蕉');
装饰器执行的顺序
//普通装饰器
function logClass1(param:any) {
console.log('类装饰器1');
}
function logClass2(param:any) {
console.log('类装饰器2');
}
function logProperty1(value:any) {
console.log('属性装饰器1');
return function (target:any,attr:any) {
}
}
function logProperty2(value:any) {
console.log('属性装饰器2');
return function (target:any,attr:any) {
}
}
function logMethod1(value:any) {
console.log('方法装饰器1');
return function (target:any,methodName:any,desc:any) {
}
}
function logMethod2(value:any) {
console.log('方法装饰器2');
return function (target:any,methodName:any,desc:any) {
}
}
function logParams1(value:any) {
console.log('方法参数装饰器1');
return function (target:any,paramsName:any,index:any) {
}
}
function logParams2(value:any) {
console.log('方法参数装饰器2');
return function (target:any,paramsName:any,index:any) {
}
}
@logClass1
@logClass2
class Person {
@logProperty1('1')
@logProperty2('2')
name:string;
constructor(name:string) {
this.name = name
}
@logMethod1('1')
@logMethod2('2')
eat(@logParams1('1')@logParams2('2')food:string ):void {
}
}
const p:any = new Person('小明');
p.eat('香蕉');
// 打印结果
属性装饰器1
属性装饰器2
方法装饰器1
方法装饰器2
方法参数装饰器1
方法参数装饰器2
类装饰器2
类装饰器1
结论
属性 > 方法 > 方法参数 > 类
存在多个类装饰器的时候先执行写在下面的
知识点
!和?的区别
属性或参数中使用 ?表示该属性或参数为可选项
属性或参数中使用 !表示强制解析(告诉typescript编译器,这里一定有值)
变量后使用 !表示类型推断排除null、undefined
联合类型
let value: number | string = '1233';