TypeScript 基础
# TypeScript
# 基础概念
# 静态类型检查(Static type-checking)
如下段代码 , 定义一个string类型变量,把它当做函数调用, 如果是js,那么在保存文件重新运行代码时,才会抛出错误。
理想情况下,我们应该有一个工具可以帮助我们,在代码运行之前就找到错误。这就是静态类型检查器比如 TypeScript 做的事情。静态类型系统(Static types systems)描述了值应有的结构和行为。一个像 TypeScript 的类型检查器会利用这个信息,并且在可能会出错的时候告诉我们:
const message = "hello!";
message();
// This expression is not callable.
// Type 'String' has no call signatures.
2
3
4
5
6
7
# 非异常失败(Non-exception)
JavaScript会告诉我们运行时的错误 运行时错误,就是 JavaScript 会在运行时告诉我们它认为的一些没有意义的事情。这些事情之所以会出现,是因为 ECMAScript 规范 (opens new window) (opens new window)已经明确的声明了这些异常时的行为。
比如在读取一个对象中不存在的属性时,js并不会报错,而是返回undefined
const user = { name: "Daniel", age: 26, }; user.location; // returns undefined
1
2
3
4
5
6而在ts中 是这样表现得
const user = { name: "Daniel", age: 26, }; user.location; // Property 'location' does not exist on type '{ name: string; age: number; }'.
1
2
3
4
5
6
7
8Ts可以捕获很多合理的错误 如:
拼写错误
const announcement = "Hello World!"; // How quickly can you spot the typos? announcement.toLocaleLowercase(); announcement.toLocalLowerCase(); // We probably meant to write this... announcement.toLocaleLowerCase();
1
2
3
4
5
6
7
8
9定义函数未被调用
function flipCoin() { // Meant to be Math.random() return Math.random < 0.5; // Operator '<' cannot be applied to types '() => number' and 'number'. }
1
2
3
4
5
6基本逻辑错误
const value = Math.random() < 0.5 ? "a" : "b"; if (value !== "a") { // ... } else if (value === "b") { // This condition will always return 'false' since the types '"a"' and '"b"' have no overlap. // Oops, unreachable }
1
2
3
4
5
6
7
8
# 类型工具
全局安装ts npm install typescript -g
类型检查器因为有类型信息,可以检查比如说是否正确获取了一个变量的属性。也正是因为有这个信息,它也可以在你输入的时候,列出你可能想要使用的属性。
# tsc
TypeScript 编译器(tsc,the TypeScript compiler)
tsc的指令:
tsc init
: 生成tsconfig.json 配置文件tsc --watch
: 实时编译tsc --noEmitOnError hello.ts
: 当存在ts报错时 不更新js
# tsconfig
tsconfig.json是ts编译器的配置文件,ts编译器可以根据它的信息来对代码进行编译
具体配置项:
{
"include":{
},
"compilerOptions":{
"target":"es2015", // 用来指定编译的es版本
"module":"es2015", // 指定要使用的模块化规范
"lib":[], // 指定项目中要使用的库
"outDir": './dist', // 编译后的文件指定的目录
"outFile":'./dist/app.js', // 将代码合并为一个文件 所有的全局作用域中的代码会合并到同一个文件中
"allowJs": true, // 是否对js文件进行编译 默认为false
"checkJs": true, // 检查js语法是否正确 默认为false
"removeCommonet":false, // 是否移除注释
"noEmit":false, // 不生成编译后的文件
"noEmitOnError":false, // 编译报错不生成编译后的文件
"strict": true, // 所有严格检查的总开关 开了就全开
"alwaysStrict":true, // 设置编译后的文件是否是严格模式 默认false
"NoImplictAny":true, // 不允许隐式的any类型
"NoImplictThis":true, // 不允许不明确类型的this
"strictNullChecks":true, // 严格的检查空值
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 显式类型
定义显式类型
function greet(person: string, date: Date) {
console.log(`Hello ${person}, today is ${date.toDateString()}!`);
}
2
3
4
我们所做的就是给 person
和 date
添加了类型注解(type annotations),描述 greet
函数可以支持传入什么样的值。你可以如此理解这个签名 (signature): greet
支持传入一个 string
类型的 person
和一个 Date
类型的 date
# 类型抹除
类型注解并不是 JavaScript 的一部分。所以并没有任何浏览器或者运行环境可以直接运行 TypeScript 代码。
所以在生成的js文件中我们的类型注解就全部被抹除了。
"use strict";
function greet(person, date) {
console.log("Hello " + person + ", today is " + date.toDateString() + "!");
}
greet("Maddison", new Date());
2
3
4
5
6
# 降级(Downleveling)
Ts再经过tsc的编译后生成了了
"Hello " + person + ", today is " + date.toDateString() + "!";
是因为模板字符串是 ECMAScript2015(也被叫做 ECMAScript 6 ,ES2015, ES6 等)里的功能,TypeScript 可将新版本的代码编译为老版本的代码,比如 ECMAScript3 或者 ECMAScript5 。这个将高版本的 ECMAScript 语法转为低版本的过程就叫做降级(downleveling) 。
Ts 默认转换为ES3
,通过tsconfig target选项可以进行配置 或者用命令行配置target tsc --target es2015 hello.ts
# 严格模式
TypeScript 有几个严格模式设置的开关。
# noImplicitAny
用来检验any
# strictNullChecks
默认情况下,像 null
和 undefined
这样的值可以赋值给其他的类型。可以让我们更方便的写一些代码。但是忘记处理 null
和 undefined
也导致了不少的 bug
# TS常见类型
#
#
# 原始类型: string
,number
和 boolean
# 数组(Array)
定义一个类似[1,2,3]
的数组 需要用到语法 number[]
这个语法可以适用于任何类型 比如 string[]
还有一种写法是泛型写法:Array<number>
let arr: number[] = [1,2,3]
let arr2: Array<number> = [1,2,3]
2
# 元组
元组实际上就是固定长度的数组
# Any
不希望一个值导致类型检查错误的时候,就可以设置为 any
// any 会把其他变量类型也影响
let a:any
let b:string
b = a
2
3
4
# unknown
unknown 表示未知类型的值
let k : unknown
k = "nk"
let j : string
j =k
// 不能将类型“unknown”分配给类型“string”。
2
3
4
5
any和unknown的区别就是 unknown不可以赋值给其他变量 实际上unknown
就是一个类型安全的any
# 枚举
# 类型注释
当你使用 const
、var
或 let
声明一个变量时,你可以选择性的添加一个类型注解,显式指定变量的类型:
let myName: string = "Alice";
TypeScript 大多数情况会自动推断类型。
# 函数
函数是 JavaScript 传递数据的主要方法。TypeScript 允许你指定函数的输入值和输出值的类型。
# 参数类型注解(Parameter Type Annotations)
当你声明一个函数的时候,你可以在每个参数后面添加一个类型注解,声明函数可以接受什么类型的参数。参数类型注解跟在参数名字后面:
// Parameter type annotation
function greet(name: string) {
console.log("Hello, " + name.toUpperCase() + "!!");
}
2
3
4
如果入参没有指定类型,会隐式推断为any
# 返回值类型注解(Return Type Annotations)
返回值的类型注解跟在参数列表后面
跟变量类型注解一样,你也不需要总是添加返回值类型注解,TypeScript 会基于它的 return
语句推断函数的返回类型。
function getFavoriteNumber(): number {
return 26;
}
2
3
4
# 匿名函数
当 TypeScript 知道一个匿名函数将被怎样调用的时候,匿名函数的参数会被自动的指定类型。
# 对象类型
// The parameter's type annotation is an object type
function printCoord(pt: { x: number; y: number }) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 3, y: 7 });
2
3
4
5
6
7
对象类型可以指定一些甚至所有的属性为可选的,你只需要在属性名后添加一个 ?
[propName:string]:any 表示任意类型的属性
let obj : {name:string,[propName:string]:any}
obj = {name:'test',ss:'tes'}
2
# 联合类型
# 定义一个联合类型(Defining a Union Type)
第一种组合类型的方式是使用联合类型,一个联合类型是由两个或者更多类型组成的类型,表示值可能是这些类型中的任意一个。这其中每个类型都是联合类型的成员(members)。
function printId(id: number | string) {
console.log("Your ID is: " + id);
}
// OK
printId(101);
// OK
printId("202");
// Error
printId({ myID: 22342 });
2
3
4
5
6
7
8
9
TypeScript 会要求你做的事情,必须对每个联合的成员都是有效的
用代码收窄联合类型,就像你在 JavaScript 没有类型注解那样使用。当 TypeScript 可以根据代码的结构推断出一个更加具体的类型时,类型收窄就会出现。 实际上就是对类型进行判断 并进行不同的操作
function welcomePeople(x: string[] | string) {
if (Array.isArray(x)) {
// Here: 'x' is 'string[]'
console.log("Hello, " + x.join(" and "));
} else {
// Here: 'x' is 'string'
console.log("Welcome lone traveler " + x);
}
}
2
3
4
5
6
7
8
9
10
如果联合类型里的每个成员都有一个属性,举个例子,数组和字符串都有 slice
方法,你就可以直接使用这个属性,而不用做类型收窄。
# 类型别名(Type Aliases)
在类型注解里直接使用对象类型和联合类型,这很方便,但有的时候,一个类型会被使用多次,此时我们更希望通过一个单独的名字来引用它。
类型别名,顾名思义,一个可以指代任意类型的名字。
type Point = {x:number; y:number;}
function getPoint(po:Point){
return `x轴${po.x},y轴${po.y}`
}
getPoint({x:1,y:2})
2
3
4
5
# 接口
接口声明(interface declaration)是命名对象类型的另一种方式:
interface Point {
x: number;
y: number;
}
function printCoord(pt: Point) {
console.log("The coordinate's x value is " + pt.x);
console.log("The coordinate's y value is " + pt.y);
}
printCoord({ x: 100, y: 100 });
2
3
4
5
6
7
8
9
10
11
12
TypeScript 只关心传递给 printCoord
的值的结构(structure)——关心值是否有期望的属性。正是这种只关心类型的结构和能力的特性,我们才认为 TypeScript 是一个结构化(structurally)的类型系统。
# 类型别名和接口的不同
类型别名和接口非常相似,大部分时候,你可以任意选择使用。接口的几乎所有特性都可以在 type
中使用,两者最关键的差别在于类型别名本身无法添加新的属性,而接口是可以扩展的。
interface Animal {
name:string
}
// 接口通过继承扩展
interface Bear extends Animal {
honey:Boolean
}
const littleBear: Bear{
name: 'xiaoxiong',
honey: true
}
// 别名通过交集拓展类型
type Animal = {
name:string
}
type Bear= Animal & {
honey:true
}
const bigBear:Bear = {
name:'大熊',
honey:false
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 对一个已经定义的接口添加新的字段
interface myWindow {
title:string
}
interface myWindow {
hyp: Number
}
const test: myWindow = {
title: 'ceshi',
hyp:1
}
// 别名类型创建以后不能更改
type MyWindow2 = {
title: string
}
type MyWindow2 = {
test: string
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 类型断言
当我们知道某个类型但是ts不知道的时候,我们使用类型断言将其指定为一个更具体的类型
如下
这两种是等价的
const myCanvas = document.getElementById('main_canvas') as HTMLCanvasElement
const myCanvas2 = <HTMLCanvasElement>document.getElementById('main_canvas')
2
谨记:因为类型断言会在编译的时候被移除,所以运行时并不会有类型断言的检查,即使类型断言是错误的,也不会有异常或者 null
产生。
TypeScript 仅仅允许类型断言转换为一个更加具体或者更不具体的类型。这个规则可以阻止一些不可能的强制类型转换:
const x = "hello" as number;
// 会提示错误
// 这时 使用双重断言
const x = (expr as any) as T; // any或者unknown
2
3
4
# 字面量类型(Literal Types)
除了常见的类型 string
和 number
,我们也可以将类型声明为更具体的数字或者字符串。
使用
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.
2
3
4
5
6
7
搭配联合类型
// 指定参数
function printXX(s:string,y:'bixv' | 'cixv' |'aixv') {
}
printXX('xx','xx')
//类型“"xx"”的参数不能赋给类型“"bixv" | "cixv" | "aixv"”的参数。
// 指定返回值
function compare(a:number,b:number): -1|1|0 {
return a===b?0:a>b?1:-1
}
2
3
4
5
6
7
8
9
10
11
还有一种字面量类型,布尔字面量。因为只有两种布尔字面量类型, true
和 false
,类型 boolean
实际上就是联合类型 true | false
的别名。
# 字面量推断(Literal Inference)
当你初始化变量为一个对象的时候,TypeScript 会假设这个对象的属性的值未来会被修改
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
// Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'.
2
3
4
5
6
7
req.method 被推断为 string ,而不是 "GET",因为在创建 req 和 调用 handleRequest 函数之间,可能还有其他的代码,或许会将 req.method 赋值一个新字符串比如 "Guess"
解决这种情况有两种方式
- 添加一个类型断言改变推断结果:
// 1
handleRequest(req.url, req.method as "GET");
// 2
const req = { url: "https://example.com", method: "GET" as "GET" };
2
3
4
- 也可以使用
as const
把整个对象转为一个类型字面量:
const req = { url: "https://example.com", method: "GET" } as const;
handleRequest(req.url, req.method);
2
as const
效果跟 const
类似,但是对类型系统而言,它可以确保所有的属性都被赋予一个字面量类型,而不是一个更通用的类型比如 string
或者 number
。
# null 和undefined
两个原始类型的值,用于表示空缺或者未初始化,他们分别是 null
和 undefined
。
config中的 strictNullChecks
决定是否判断
TypeScript 提供了一个特殊的语法,可以在不做任何检查的情况下,从类型中移除 null
和 undefined
就是在任意表达式后面写上 !
这是一个有效的类型断言,表示它的值不可能是 null
或者 undefined
:
function liveDanger(x?:number | null) {
// ! 代表断言参数x不可能是 null或者undefined
console.log(x!.toFixed)
}
2
3
4
# 枚举
枚举是 TypeScript 添加的新特性,用于描述一个值可能是多个常量中的一个。不同于大部分的 TypeScript 特性,这并不是一个类型层面的增量,而是会添加到语言和运行时
# 不太常用的原语
# BigInt
ES2020 引入原始类型 BigInt
,用于表示非常大的整数:
// Creating a bigint via the BigInt function
const oneHundred: bigint = BigInt(100);
// Creating a BigInt via the literal syntax
const anotherHundred: bigint = 100n;
2
3
4
5
6
# symbol
const firstName = Symbol("name");
const secondName = Symbol("name");
if (firstName === secondName) {
// This condition will always return 'false' since the types 'typeof firstName' and 'typeof secondName' have no overlap.
// Can't ever happen
}
2
3
4
5
6
7
8
# 类型收窄
# in 操作符收窄
JavaScript 中有一个 in
操作符可以判断一个对象是否有对应的属性名。TypeScript 也可以通过这个收窄类型。
type Fish = {swim:()=>void};
type Bird = {fly:()=>void};
func
2
3
# 函数
# 调用签名
函数除了可以被调用,也可以有自己的属性值,这时需要另一种写法来支持属性的描述
type desc = {
description: string,
(arg:number):string
}
function descption(fn:desc) {
return `${fn.description} ${fn(4)}`
}
function toDesc(num:number){
return `is ${num}`
}
toDesc.description = '这时描述'
console.log( descption(toDesc))
2
3
4
5
6
7
8
9
10
11
12
可以看到别名的定义写法是在参数列表和返回类型之间使用:
而不是=>
# 泛型函数
在定义函数或者类 , 遇到类型不明确时, 可以使用泛型
function foo(a:any):any {return}
function fn<T>(a:T):T{return a}
2
对比上面的两个函数定义 可以看出 如果使用any 并不能知道返回值和参数的类型是一致的
泛型的使用:
泛型可以同时指定多个
// 直接使用具有泛型的函数
// 直接使用
fn(1)
// 指定类型使用
fn<string>('hhh')
// 定义多个泛型
function foo<T,U>(a:T,b:U):U{
console.log(a)
return b
}
// 继承泛型
interface inter{
length: number
}
// T extends inter 表示 T必须是inter 的实现类(子类)
function boo<T extends inter>(a:T) :number{
return a.length
}
// 传一个字符串 字符串有length属性
boo<string>('xxx')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
泛型的作用就是在类型不明确时 定义一个变量 来表示类型