# TypeScript 基础
# 使用ts原因
在编写阶段就可以检测出代码的大部分问题,实现的一种静态检查的类型的工具。
一个静态类型需要标记出哪些代码是一个错误,哪怕实际生效的js并不会立刻报错。
export const user = {
name: "Daniel",
age: 26,
};
user.love ='money'// 类型“{ name: string; age: number; }”上不存在属性“love”。
- 比如拼写错误:
const announcement = "Hello World!";
// How quickly can you spot the typos?
announcement.toLocaleLowercase();
// We probably meant to write this...
announcement.toLocaleLowerCase();
- 函数未被调用:
function flipCoin() {
// Meant to be Math.random()
return Math.random < 0.5;
// Operator '<' cannot be applied to types '() => number' and 'number'.
}
- 基本的逻辑错误:
const value = Math.random() < 0.5 ? "a" : "b";
if (value !== "a") {
// ...
} else if (value === "b") { //此条件将始终返回 "false",因为类型 ""a"" 和 ""b"" 没有重叠。
// This condition will always return 'false' since the types '"a"' and '"b"' have no overlap.
}
这些都可提前发现。
# ts变量声明&&类型注解&&类型推断
变量是一种使用方便的占位符,用于引用计算机内存地址。变量声明语法:冒号 : 前面是变量名称,后面是变量类型。
- 声明变量的类型及初始值,声明了类型后TypeScript就会进行类型检测,声明的类型可以称之为类型注解;
var/let/const 标识符: 数据类型 = 赋值;
// var [变量名] : [类型] = 值;//:后注解类型
var uname:string = "Runoob";
TS的const声明必须初始化赋值
let count: number;//类型注解,先不赋值,之后赋值需遵从数据定义的类型
count = 1;
let countInference = 123;//类型推论,如明确知道,可省略让ts自己判断
# 类型断言
TS允许覆盖它的推断.类型断言主要用于当TS推断出来类型并不满足需求,需要手动指定一个类型。
有使用关键字 as 和标签 <> 两种方式,因后者会与JSX 语法冲突,优先使用 as 来进行类型断言。
interface User {
nickname: string,
admin: boolean,
groups: number[]
}
const user = {} as User //这里通过 as 关键字进行类型断言,将变量 user 的类型覆盖为 User 类型
//const user = <User>{} // User类型 使用 <User>{} 这种标签形式,将变量 user 强制断言为 User 类型
user.nickname = 'Evan'
user.admin = true
user.groups = [2, 6]
/**
* 类型断言 “尖括号”语法
*/
let strLength: number = (<Array<number>>someValue).length;
let strLength1: number = (<string[]>someValue).length;
// let strLength2: number = (someValue as string[]).length;
有时候TypeScript无法获取具体的类型信息,需要使用类型断言(Type Assertions)。比如通过 document.getElementById,TypeScript只知道该函数会返回 HTMLElement ,但并不知道它具体的类型:
const el = document.getElementById("why") as HTMLImageElement
el.src = "url地址"
# 双重断言
双重断言极少有应用场景
interface User {
nickname: string,
admin: boolean,
group: number[]
}
const user = 'Evan' as any as User
typeScript 仅仅允许类型断言转换为一个 更加具体或者更不具体 的类型。这个规则可以阻止一些不可能的强制类型转换,比如:
const x = "hello" as number;
// Conversion of type 'string' to type 'number' may be a mistake because neither type
// sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
有的时候,这条规则会显得非常保守,阻止了原本有效的类型转换。如果发生了这种事情,可以使用双重断言,先断言为 any (或者是 unknown),然后再断言为期望的类型:
export let t1 =('jjj' as any) as number
t1=40
console.log(t1)
非必须不要这样操作!
# 非空断言 !
如果编译器不能够去除 null 或 undefined,可以使用非空断言 ! 手动去除。
function fixed(name: string | null): string {
function postfix(epithet: string) {
// postfix() 是一个嵌套函数,因为编译器无法去除嵌套函数的 null (除非是立即调用的函数表达式),
// 所以 TypeScript 推断第 3 行的 name 可能为空。
return name!.charAt(0) + '. the ' + epithet; // name 被断言为非空
}
// 而 name = name || "Bob" 这行代码已经明确了 name 不为空,所以可以直接给 name 断言为非空(第 3 行)
name = name || "Bob"
return postfix("great")
}
let s=fixed("ttt")
console.log(s)//t. the great
# TS数据类型
object可以用来声明对象,当然也可以用具体的对象类型来声明
let f:object ={}
let s:any;// √
let a:null
s.aaa //√
a //在赋值前使用了变量“a”。error
如果只声明不赋值,直接使用的话,当声明类型为undefined或者any时才可以,即使是null,也需要赋值
- symbol:使用 Symbol() 创建新的 symbol 类型
const sym1: symbol = Symbol()
const sym2: symbol = Symbol('foo')
const sym3: symbol = Symbol('foo')
# TS undefined
在js中当定义了一个变量,但没有赋予任何值的时候,他就是undefined类型。
新建demo01.ts文件,下入下面代码:
//声明数值类型的变量age,但不予赋值
var age:number
console.log(age) //在赋值前使用了变量“age”。error
写完后保存代码,进行运行任务,然后生成demo01.js,在终端中使用node demo01.js来进行查看运行结果。控制台输出了undefined,跟预想的一样。这只是在tsc demo01.ts之后得到的js才生效。ts这样写会报异常,运行ts-node demo01.ts会报错。
# ts枚举值和反向映射
- 枚举类型用于定义数值集合
enum Color {Red, Green, Blue};
let c: Color = Color.Blue;
console.log(c); // 输出 2
枚举还可以自定义对应下标,如果设置的有字符串等,则之后的值都需要设置具体的索引值,否则会报错,如果为数字,如果之后的都没有设,则可以递增。如果多个索引一致取最后一个。
enum Color {
Red='21',
Green=3,
Blue,
Orange=3
};
console.log(Color[4])//Blue
console.log(Color.Orange)// 3
console.log(Color.Green)// 3
console.log(Color[3])// Orange
- 反向映射:就是指枚举的取值,不但可以正向的 Months.Jan 这样取值,也可以反向的 Months[1] 这样取值。
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
console.log(Months)
/*{
'1': 'Jan',
'2': 'Feb',
'3': 'Mar',
'4': 'Apr',
Jan: 1,
Feb: 2,
Mar: 3,
Apr: 4
}
*/
console.log(Months.Mar === 3) // true
// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3]) // 'Mar'
// 所以
console.log(Months.Mar === 3) // true
console.log(Months[3] === 'Mar') // true
var Months;
(function (Months) {
Months[Months["Jan"] = 1] = "Jan";
Months[Months["Feb"] = 2] = "Feb";
Months[Months["Mar"] = 3] = "Mar";
Months[Months["Apr"] = 4] = "Apr";
})(Months || (Months = {}));
console.log(Months);
console.log(Months.Mar === 3); // true
// 那么反过来能取到 Months[3] 的值吗?
console.log(Months[3]); // 'Mar'
// 所以
console.log(Months.Mar === 3); // true
console.log(Months[3] === 'Mar'); // true
- 枚举合并:分开声明名称相同的枚举类型,会自动合并
enum Months {
Jan = 1,
Feb,
Mar,
Apr
}
enum Months {
May = 5,
Jun
}
console.log(Months.Apr) // 4
console.log(Months.Jun) // 6
// type Direction = "left" | "Right" | "Top" | "Bottom"
enum Direction {
LEFT = "LEFT",
RIGHT = "RIGHT",
TOP = "TOP",
BOTTOM = "BOTTOM"
}
let name: string = "abc"
let d: Direction = Direction.BOTTOM
function turnDirection(direction: Direction) {
console.log(direction)
switch (direction) {
case Direction.LEFT:
console.log("改变角色的方向向左")
break;
case Direction.RIGHT:
console.log("改变角色的方向向右")
break;
case Direction.TOP:
console.log("改变角色的方向向上")
break;
case Direction.BOTTOM:
console.log("改变角色的方向向下")
break;
default:
const foo: never = direction;
break;
}
}
turnDirection(Direction.LEFT)
turnDirection(Direction.RIGHT)
turnDirection(Direction.TOP)
turnDirection(Direction.BOTTOM)
export {}
- 字符串枚举成员不会生成反向映射。
- 枚举类型被编译成一个对象,它包含了正向映射( name -> value)和反向映射( value -> name)。
- 通过关键字 enum 来声明枚举类型。
- TypeScript 仅支持基于数字和字符串的枚举。
- 通过枚举类型编译后的结果,了解到其本质上就是 JavaScript 对象。
# TS never类型
- never 是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。这意味着声明为 never 类型的变量只能被 never 类型所赋值,在函数中它通常表现为抛出异常或无法执行到终止点(例如无限循环)
//抛出异常的函数表达式,其函数返回值类型为 never
function error(message:string): never {
throw new Error(message)
}
举个例子,当有一个union type:
interface Foo {
type: 'foo'
}
interface Bar {
type: 'bar'
}
type All = Foo | Bar
在 switch 当中判断 type,TS 是可以收窄类型的 (discriminated union):
function handleValue(val: All) {
switch (val.type) {
case 'foo':
// 这里 val 被收窄为 Foo
break
case 'bar':
// val 在这里是 Bar
break
default:
// val 在这里是 never
const exhaustiveCheck: never = val
break
}
}
注意在 default 里面把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天别人改了 All 的类型:
type All = Foo | Bar | Baz
他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,这个时候在default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。所以通过这个办法,可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。
# ts类型保护(类型收窄)
类型保护就是让编译器缩小类型范围(类型收窄),在编译阶段规避掉一些不必要的错误,利用 typeof instanceof in 或者字面量等
class User {
public nickname: string | undefined
public group: number | undefined
}
class Log {
public count: number = 10
public keyword: string | undefined
}
function typeGuard(arg: User | Log) {
if (arg instanceof User) {
arg.count = 15 // Error, User 类型无此属性
}
if (arg instanceof Log) {
arg.count = 15 // OK
}
}
ts会要求做的事情,必须对每个联合的成员都是有效的。举个例子,如果有一个联合类型 string | number , 不能使用只存在 string 上的方法:
function printId(id: number | string) {
console.log(id.toUpperCase());
//类型“string | number”上不存在属性“toUpperCase”。
//类型“number”上不存在属性“toUpperCase”。
}
解决方案是用代码收窄联合类型,就像你在 JavaScript 没有类型注解那样使用。当 TypeScript 可以根据代码的结构推断出一个更加具体的类型时,类型收窄就会出现。
举个例子,TypeScript 知道,对一个 string 类型的值使用 typeof 会返回字符串 "string":
function printId(id: number | string) {
if (typeof id === "string") {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
- 利用typeof:注意null这种特殊情况
function printAll(strs: string | string[] | null) {
if (strs&&typeof strs === "object") {
for (const s of strs) {
// Object is possibly 'null'.
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
} else {
// do nothing
}
}
- 真值收窄:可以在条件语句中使用任何表达式,比如 && 、||、! 等,举个例子,像 if 语句就不需要条件的结果总是 boolean 类型(注意:
在基本类型上的真值检查很容易导致错误)
function printAll(strs: string | string[] | null) {
if (strs) {
if (typeof strs === "object") {
for (const s of strs) {
console.log(s);
}
} else if (typeof strs === "string") {
console.log(strs);
}
}
}
DANGER
如果传入的是空字符串,真值检查判断为 false,就会进入错误的处理分支。
- 等值收窄:使用 switch 语句和等值检查比如 === !== == != 去收窄类型。
- in操作符收窄:in 操作符可以判断一个对象是否有对应的属性名
type Fish = { swim: () => void };
type Bird = { fly: () => void };
type Human = { swim?: () => void; fly?: () => void };
function move(animal: Fish | Bird | Human) {
if ("swim" in animal) {
animal; // (parameter) animal: Fish | Human
} else {
animal; // (parameter) animal: Bird | Human
}
}
- instanceof 收窄
function printTime(time: string | Date) {
if (time instanceof Date) {
//(parameter) time: Date
console.log(time.toUTCString())
} else {
//(parameter) time: string
console.log(time)
}
}
- 赋值语句收窄:TypeScript 可以根据赋值语句的右值,正确的收窄左值。
let x = Math.random()*10>5?100:'only';
x=1000
//x=true//不能将类型“boolean”分配给类型“string | number”。
- 控制流分析:基于可达性(reachability) 的代码分析就叫做控制流分析(control flow analysis)。在遇到类型保护和赋值语句的时候,TypeScript 就是使用这样的方式收窄类型。
- 类型判断式(类型谓词 type predicates):所谓 predicate 就是一个返回 boolean 值的函数。
Ts 中的 is 关键字,它被称为类型谓词,用来判断一个变量属于某个接口或类型。
// 判断参数是否为string类型, 返回布尔值
function isString(s:unknown):boolean{
return typeof s === 'string'
}
// 参数转为大写函数
// 直接使用转大写方法报错, str有可能是其他类型
function upperCase(str:unknown){
str.toUpperCase()
// 类型“unknown”上不存在属性“toUpperCase”。
}
// 判断参数是否为字符串,是在调用转大写方法
function ifUpperCase(str:unknown){
if(isString(str)){
str.toUpperCase()
// (parameter) str: unknown
// 报错:类型“unknown”上不存在属性“toUpperCase”
}
}
此时,可以在判断是否为string类型的函数返回值类型使用is关键词(即类型谓词)
// 判断参数是否为string类型, 返回布尔值
function isString(s:unknown):s is string{
return typeof s === 'string'
}
// 判断参数是否为字符串,是在调用转大写方法
function ifUpperCase(str:unknown){
if(isString(str)){
str.toUpperCase()
// (parameter) str: string
}
}
s is string不仅返回boolean类型判断参数s是不是string类型, 同时明确的string类型返回到条件为true的代码块中.
因此当判断条件为true, 即str为string类型时, 代码块中str类型也转为更明确的string类型
类型谓词的主要特点是:
- 返回类型谓词,如 s is string;
- 包含可以准确确定给定变量类型的逻辑语句,如 typeof s === 'string'。
利用类型谓词实际应用
// 接口 interfaceA
interface interfaceA {
name: string;
age: number;
}
// 接口 interfaceB
interface interfaceB {
name: string;
phone: number;
}
// 推断类型
const obj1 = { name: "andy", age: 2 };
// const obj1: {name: string;age: number;}
const obj2 = { name: "andy", phone: 2 };
// const obj2: {name: string;phone: number;}
// 创建数组
// arr1, 创建两个interfaceA[]数组, 数组每一项都是 obj1
const arr1 = new Array<interfaceA>(2).fill(obj1);
// const arr1: interfaceA[]
// arr2, 创建两个interfaceB[]数组, 数组每一项都是 obj2
const arr2 = new Array<interfaceB>(2).fill(obj2);
// const arr2: interfaceB[]
// 合并两种类型数组,
// arr3类型就是一个联合数组
const arr3 = [...arr1, ...arr2];
// const arr3: (interfaceA | interfaceB)[]
const target = arr3[0];
// const target: interfaceA | interfaceB
// Ok获取两个结构共有的属性
console.log(target.name);
// 获取两个接口不同的属性报错:
console.log(target.phone);
// 报错: 类型“interfaceA”上不存在属性“phone”
console.log(target.age);
// 报错: 类型“interfaceB”上不存在属性“age”
示例代码中我们定义了一个接口interfaceA,一个接口interfaceB,各自对应一个具体的数据类型,但有时需要将这2个数据数组进行合并,TypeScript会自动推断这个数据的类型为联合类型数组
const arr3: (interfaceA | interfaceB)[]
通过数组操作取出数组里的元素时,这个元素的类型其实也是这个联合类型,需要取值的时候,由于name属于interfaceA和interfaceB共有的,即交叉属性
// 类型“interfaceA | interfaceB”上不存在属性“phone”。
// 类型“interfaceA”上不存在属性“phone”。ts(2339)
- 解决方案一:将所有非交叉属性设置 为可选属性:这种方式也不是特别好, 因为对于interfaceB来说,可能phone属性就是必选的,定义成可选属性是一种逃避,且不安全
// 接口 interfaceA
interface interfaceA {
name: string;
age?: number;
}
// 接口 interfaceB
interface interfaceB {
name: string;
phone?: number;
}
- 解决方案二:断言:但也不是特别好,因为难道在一个作用域下,每次取值都要断言,麻烦且有风险,
// 使用断言
console.log((target as interfaceB).phone);
console.log((<interfaceA>target).age);
- 解决方案三:条件判断:缺点同上
// 通过使用in运算符 的条件判断, 缩小target类型
if('phone' in target){
console.log(target.phone);
// const target: interfaceB
}
if('age' in target){
console.log(target.age);
// const target: interfaceA
}
- 使用TypeScript中的自定义类型保护和类型谓词
需要创建一个函数,在这个函数的方法体中,不仅要检查target 变量是否含有 age属性,而且还要告诉 TypeScript 编译器,如果上述逻辑语句的返回结果是 true,那么当前判断的target 变量值的类型是 interfaceA类型
创建一个自定义类型保护函数 —— isInterfaceA,它的具体实现如下:
// 联合类型
type interfaceAB = interfaceA | interfaceB;
// 自定义类型保护函数
const isInterfaceA = (item: interfaceAB): item is interfaceA => {
return (item as interfaceA).age !== undefined;
};
// 判断target 属于哪个类型
if (isInterfaceA(target)) {
console.log(target.age); //target的类型为interfaceA
} else {
console.log(target.phone); //target的类型为interfaceB
}
通过泛型解决类型谓词复用问题 如果你要检查的类型很多,那么为每种类型创建和维护唯一的类型保护可能会变得很繁琐。针对这个问题,我们可以利用 TypeScript 的另一个特性 —— 泛型,来解决复用问题:
例如:定义通用类型保护函数
// 通过泛型定义通用类型保护函数
function isOfType<T>(
target: unknown,
prop: keyof T
): target is T {
return (target as T)[prop] !== undefined;
}
// 类型保护
if (isOfType<interfaceA>(target, "age")) {
console.log(target.age);
}
if (isOfType<interfaceB>(target, "phone")) {
console.log(target.phone);
}
is 关键字经常用来封装”类型保护函数”,通过和函数返回值的比较,从而缩小参数的类型范围,所以类型谓词 is 也是一种类型保护。
- never收窄:参见never收窄
# TS unknown类型
unknown类型:unknown类型只能分配给 any类型和unknown类型 本身
unknown类型是TS3新增的类型,这个类型与any类型类似,可以设置任何的类型值,随后可以更改类型。因此可以将变量先设置为字符串类型,然后再将其设置为数字类型,如果事先不检查类型,使用any类型,调用了不存在的方法,编译时不会报错,代码运行时才会发现错误。但是使用unknown 类型不一样,如果不进行类型判断,执行相关操作编译器就会报错。
// 编译器能顺利编译,当运行 node any,会报异常
let val: any = 22;
val = "string value";
val = new Array();
val.doesnotexist(33);
console.log(val);
// 编译时Property 'push' does not exist on type 'unknown'.
let val: unknown = 22;
val = "string value";
val = new Array();
val.push(33);
console.log(val);
let val: unknown = 22;
val = "string value";
val = [];
if (val instanceof Array) {
val.push(33);
console.log(val);
}
let value: unknown
let value1: unknown = value // OK
let value2: any = value // OK
let value3: boolean = value // Error
let value4: number = value // Error
let value5: string = value // Error
let value6: object = value // Error
let value7: any[] = value // Error
let value: unknown
value = true // OK
value = 10 // OK
value = [] // OK
value = null // OK
value = new TypeError() // OK
......
unknown 类型在被确定为某个类型之前,不能被进行诸如函数执行、实例化等操作,一定程度上对类型进行了保护。
# TS联合类型 |
var val:string|number
- 如果一个值是联合类型,
那么只能访问联合类型的共有属性或方法。
interface Dog {
name: string;
eat: () => void;
destroy: () => void;
}
interface Cat {
name: string;
eat: () => void;
climb: () => void;
}
let pet: Dog | Cat;
pet!.name; // OK
pet!.eat(); // OK
pet!.climb(); // Error
- 如果创建一个pet对象,只写了公共的方法,如果同时没有Dog1的destroy和Cat1的climb,就会报错,
需要选择一个存在,也不能两个同时存在。
interface Dog1 {
name: string;
eat: () => void;
destroy: () => void;
}
interface Cat1 {
name: string;
eat: () => void;
climb: () => void;
}
let pet: Cat1|Dog1 ;
// 不能将类型“{ name: string; eat(): void; }”分配给类型“Dog1 | Cat1”。
// 类型 "{ name: string; eat(): void; }" 中缺少属性 "climb",但类型 "Cat1" 中需要该属性。ts(2322)
pet ={
name:'32',
eat(){
},
// destroy(){
// console.log('destory')
// },
// climb(){
// console.log('climb')
// }
}
console.log(pet)
// pet.destroy()
export {pet}
# 可辨识联合
函数 area() 的参数是一个 Rectangle | Circle | Parallelogram 联合类型。
其中,每个接口都有一个 type 属性,根据其不同的字符串字面量类型引导到不同的 case 分支,这种情况我们称之为 『可辨识联合(Discriminated Union)』
interface Rectangle {
type: "rectangle";
width: number;
height: number;
}
interface Circle {
type: "circle";
radius: number;
}
interface Parallelogram {
type: "parallelogram";
bottom: number;
height: number;
}
function area(shape: Rectangle | Circle | Parallelogram) {
switch (shape.type) {
case "rectangle":
return shape.width * shape.height;
case "circle":
return Math.PI * Math.pow(shape.radius, 2);
case "parallelogram":
return shape.bottom * shape.height;
}
}
const shape: Circle = {
type: "circle",
radius: 10,
};
console.log(area(shape));
# TS 交叉类型 &
交叉类型 & :& 的意思理解成 and ,A & B 表示同时包含 A 和 B 的结果
interface Admin {
id: number;
administrator: string;
timestamp: string;
}
interface User {
id: number;
groups: number[];
createLog: (id: number) => void;
timestamp: number;
}
let t: Admin & User;
t!.administrator; // 合法 Admin.administrator: string
t!.groups; // 合法 User.groups: number[]
t!.id; // 合法 id: number
t!.timestamp; // 合法 timestamp: never
- 应用场景:混入之类的
function extend<T, U>(first: T, second: U): T & U {
for(const key in second) {
(first as T & U)[key] = second[key] as any
}
return first as T & U
}
class Person {
constructor(public name: string) { }
}
class ConsoleLogger {
log() {
console.log('log')
}
}
let jim = extend(new Person('Jim'), new ConsoleLogger())
let n = jim.name
jim.log()//log
合并后即可访问 Person 类实例的 name 属性,也可以调用 ConsoleLogger 类实例的 log() 方法。
# TS 类型别名
顾名思义,一个可以指代任意类型的名字,类型别名会给类型起个新名字。类型别名有时和接口很像,但是 可以作用于原始值,联合类型,元组以及其它任何需要手写的类型。
类型别名不会新建一个类型,而是创建一个新名字来引用此类型。
type brand = string
type used = true | false
const str: brand = 'imooc'
const state: used = true
interface Admin {
id: number;
administrator: string;
timestamp: string;
}
interface User {
id: number;
groups: number[];
createLog: (id: number) => void;
timestamp: number;
}
type T = Admin & User;
type Tree<T, U> = {
left: T,
right: U
}
- 接口可以实现 extends 和 implements,类型别名可以借助&进行继承,实现类应用type也可以借助implements
- 类型别名并不会创建新类型,是对原有类型的引用,而接口会定义一个新类型。
- 接口只能用于定义对象类型,而类型别名的声明方式除了对象之外还可以定义交叉、联合、原始类型等。
- 类型别名是最初 TypeScript 做类型约束的主要形式,后来引入接口之后,TS推荐尽可能的使用接口来规范代码。
- 字面量类型:字面量(literal)是用于表达源代码中一个固定值的表示法(notation)
let protagonist: 'Sherlock'
protagonist = 'Sherlock'
protagonist = 'Watson' // Error, Type '"Watson"' is not assignable to type '"Sherlock"'
# TS索引类型 Index Types
是对象类型的高级编制,通过使用索引类型,TS编译器能够检查使用动态属性名的代码,在实际开发中,一般会结合索引类型查询和索引访问操作符使用。
- 索引类型可以让 TS 编译器覆盖检测到使用了动态属性名的代码
- 索引类型查询操作符 keyof 和索引访问操作符 T[K]
- keyof 可以获取对象/接口的 可访问索引字符串字面量类型
interface User {
id: number,
phone: string,
nickname: string,
readonly department: string,
}
class Token{
private secret: string | undefined
public accessExp: number = 60 * 60
public refreshExp: number = 60 * 60 * 24 * 30 * 3
}
//let user: keyof User // let user: "id" | "phone" | "nickname" | "department"
let user: keyof User ='nickname'
console.log(user)//nickname
type token = keyof Token // type token = "accessExp" | "refreshExp"
class Token{
public secret: string = 'ixeFoe3x.2doa'
public accessExp: number = 60 * 60
public refreshExp: number = 60 * 60 * 24 * 30 * 3
public br :boolean =false
}
type token = keyof Token
type valueType = Token[token] // type valueType = string | number|boolean
type secret = Token['secret'] // type secret = string
//参见 ts-extends类型约束
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n])
}
interface Person {
name: string
position: string
age: number
}
let person: Person = {
name: 'Evan',
position: 'Software Engineer',
age: 27
}
let values: unknown[] = pluck(person, ['name', 'age'])
console.log(values)//[ 'Evan', 27 ]
# K extends keyof T和keyof T注意区别
keyof T 是 T 类型的键集,比如:上面因为直接用 keyof 看不出来 KA1 的类型集,用 Extract 排除一个不存在的类型 unknown 之后可以看出来。实际 KA1 和 KA 是等价的类型。
in 可以理解为 for ... in,表示从 keyof T 中去遍历每一个类型,用上述的例子就是分别是 "a"、"b" 和 "c" 类型。注意这里他们不是 string 类型,而是 literal string 类型,是某个具体的字符串类型。比如 "a" 类型的变量就只能是 "a" 值。
这里提到了“子类型”,与之相对的是“父类型”。简单的理解,可以从逻辑上这么认为
子类型的值一定也是父类型。比如“猫”是“动物”的子类型,一只具体的 “猫”一定也是“动物” 反之不成立,某个具体的“动物”不一定是“猫”,比如一只具体的“狗”。 而 K extend keyof T 表示 K 是 T 的子类型,这里是一个类型约束声明。比如 type T = "a" | "b" | "c";,那么 K 可以是 "a",也可以是 "a" | "c" 或者 "a" | "b" | "c" 等
# TS映射类型(in keyof) Partial Readonly
ts可以使用泛型来做类型映射,将对象或数组中类型转换为另一个类型。(Mappod type通过转换旧类型中的每个属性来创建新类型)
映射类型可以将已知类型的每个属性都变为可选的或者只读的。使用 Readonly 与 Partial 关键字
interface Person{
name: string
age: number
}
type PersonOptional = Partial<Person>
type PersonReadonly = Readonly<Person>
type PersonOptional = {
name?: string
age?: number
}
type PersonReadonly = {
readonly name: string
readonly age: number
}
- 两个关键字的源码分析
type Readonly<T> = {
readonly [K in keyof T]: T[K]
}
type Partial<T> = {
[K in keyof T]?: T[K]
}
- 首先,先执行 keyof Props 获取对象类型 Props所有建的联合类型('a'|'b'|'c')
- 然后,key in ...就表示Key可以是Props中所有的键名称中的任意一个。
# 条件类型
条件类型就是在初始状态并不直接确定具体类型,而是通过一定的类型运算得到最终的变量类型
条件类型用来表达非均匀类型映射,可以根据一个条件表达式来进行类型检测,从两个类型中选出其中一个:
T extends U ? X : Y
类似三目运算符:若 T 是 U 的子类型,则类型为 X,否则类型为 Y。若无法确定 T 是否为 U 的子类型,则类型为 X | Y。
declare function f<T extends boolean>(x: T): T extends true ? string : number
//在条件不确定的情况下,得到了联合类型 string | number
const x = f(Math.random() < 0.5) // const x: string | number
const y = f(true) // const y: string
const z = f(false) // const z: number
# 可分配条件类型
在条件类型 T extends U ? X : Y 中,当泛型参数 T 取值为 A | B | C 时,这个条件类型就等价于 (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y),这就是可分配条件类型。
可分配条件类型(distributive conditional type)中被检查的类型必须是裸类型参数(naked type parameter)。裸类型表示没有被包裹(Wrapped) 的类型,(如:Array<T>、[T]、Promise<T> 等都不是裸类型),简而言之裸类型就是未经过任何其他类型修饰或包装的类型。
- 应用场景
Exclude<T, U>– 从 T 中剔除可以赋值给 U 的类型。Extract<T, U>– 提取 T 中可以赋值给 U 的类型。NonNullable<T>– 从 T 中剔除 null 和 undefined。ReturnType<T>– 获取函数返回值类型。InstanceType<T>– 获取构造函数类型的实例类型。
type T00 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c' | 'f'> // 'b' | 'd'
Exclude实现源码:
type Exclude<T, U> = T extends U ? never : T;
# ts字面量类型
可以将类型声明为更具体的数字或者字符串
let x: "hello" = "hello";
// OK
x = "hello";
// ...
x = "howdy";
// Type '"howdy"' is not assignable to type '"hello"'.
如果以const且没加类型注解,默认是字面量类型
字面量类型本身并没有什么用,如果结合联合类型,就显得有用多了。举个例子,当函数只能传入一些固定的字符串时:
function printText(s: string, alignment: "left" | "right" | "center") {
// ...
}
printText("Hello, world", "left");
# 字面量赋值[函数对象参数躲避检查]
TypeScript在字面量直接赋值的过程中,为了进行类型推导会进行严格的类型限制。但是之后如果将一个变量标识符赋值给其他的变量时,会进行freshness擦除操作。
interface IPerson {
name: string
age: number
height: number
}
function printInfo(person: IPerson) {
console.log(person)
}
// 代码会报错
// printInfo({
// name: "why",
// age: 18,
// height: 1.88,
// address: "广州市"
// })
const info = {
name: "why",
age: 18,
height: 1.88,
address: "广州市"
}
printInfo(info)
# 字面量推断
当初始化变量为一个对象的时候,TypeScript 会假设这个对象的属性的值未来会被修改.
declare function handleRequest(url: string, method: "GET" | "POST"): void;
const req = { url: "https://example.com", method: "GET" };
handleRequest(req.url, req.method);
//req.method: 类型“string”的参数不能赋给类型“"GET" | "POST"”的参数。
在上面这个例子里,req.method 被推断为 string ,而不是 "GET",因为在创建 req 和 调用 handleRequest 函数之间,可能还有其他的代码,或许会将 req.method 赋值一个新字符串比如 "Guess" 。所以 TypeScript 就报错了。
- 解决方案1:利用const声明为常量,下图可以看出,method就是POST常量不会改变

type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
type Request = {
url: string,
method: Method
}
const options = {
url: "https://www.coderwhy.org/abc",
method: "POST"
} as const
request(options.url, options.method)
- 解决方案2:type约束
type Method = 'GET' | 'POST'
function request(url: string, method: Method) {}
type Request = {
url: string,
method: Method
}
const options:Request = {
url: "https://www.coderwhy.org/abc",
method: "POST"
}
request(options.url, options.method)
- 解决方案3:as类型断言
request(options.url, options.method as Method)
# ts is关键字
is 关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。
使用原因: 但是由于函数嵌套 TypeScript 不能进行正确的类型判断。
通过 is 关键字将类型范围缩小为 string 类型,这也是一种代码健壮性的约束规范。
# ts infer关键字 推断
- 想在获取数组里的元素类型
type Ids = number[];
type Names = string[];
type Unpacked<T> = T extends Names ? string : T extends Ids ? number : T;
type idType = Unpacked<Ids>; // idType 类型为 number
type nameType = Unpacked<Names>; // nameType 类型为string
let fnum:idType = 1000
console.log(fnum)//1000
如果T是某个待推断类型的数组,则返回推断的类型,否则返回T
type ElementOf<T> = T extends Array<infer E> ? E : boolean;
type Tuple = string[];
// Tuple是string[],推断出E是string
type TupleToUnion = ElementOf<Tuple>
let k20t1:TupleToUnion ='30'
console.log(k20t1)//“30”
// number extends Array<?> =>不成立,走boolean
let k20t2:ElementOf<number> = true
console.log(k20t2)//56423 数字
infer可以推断出联合类型
type Foo21<T> = T extends { a: infer U; b: infer U } ? U : never;
type T11 = Foo21<{ a: string; b: number }>; // T11类型为 string | number
let a1:T11 = '30'
let b1:T11 = 'demo'
let d1:Foo21<{ a: string; b: number }> = 1999
//let c1:T11 = true //Type 'boolean' is not assignable to type 'string | number'.
console.log(a1,b1,d1)//"30" "demo" 1999
type T1 = { name: string };
type T2 = { age: number };
type K2<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
? U
: never;
interface Props {
a: (x: T1) => void;
b: (x: T2) => void;
}
type k3 = K2<Props>;
const app: k3 = {
name: "k17",
age: 30,
};
console.log(app); //{ name: 'k17', age: 30 }
容易混淆的点
- TypeScript 中描述类型要用 小写 ,比如 boolean、number、string等;
- 大写开头的如 Boolean、Number、String 代表的是 JavaScript 的构造函数:
let a1: Number = new Number('10') // a === 10 为 false
let b1: number = Number('10') // b === 10 为 true
//let c1: number = new Number('10')
// 不能将类型“Number”分配给类型“number”。“number”是基元,但“Number”是包装器对象。如可能首选使用“number”。
// a1 instanceof Number // true
console.log('a1',typeof a1)
console.log('b1',typeof b1)
// b1 instanceof Number // false
//The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter.
第 1 行,通过 new Number('10') 得到的是一个构造函数,本质是一个对象。
第 2 行,Number('10') 与 10 都是声明一个数字 10 的方法,本质就是一个数字。
第 4 - 5 行,instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。a 是一个对象,它的 __proto__ 属性指向该对象的构造函数的原型对象 Number,所以为 true。b 是一个数字,所以为 false。
当用构造函数定义的a。虽然是对象,但是a.__proto__会被认为错误,提示a是数字类型 ,可以通过any强制修改类型
let a: Number = new Number('10') // a === 10 为 false
let b: number = Number('10') // b === 10 为 true
// a instanceof Number // true
// Object.getPrototypeOf(a)
console.log(a===10)
console.log(b===10)
console.log(typeof a )
// console.log(a.__proto__)//类型“Number”上不存在属性“__proto__”。
console.log((a as any).__proto__ === Object.getPrototypeOf(a))//true
# keyof和typeof在ts中应用
如果该类型具有字符串或数字索引签名,则keyOf将返回这些类型:
type Arrayish = { [n: number]: unknown };
type A = keyof Arrayish;
// type A = number
type Mapish = { [k: string]: boolean };
type M = keyof Mapish;
//type M = string | number
请注意,在本例中,M是字符串|数字-这是因为JavaScript对象键始终被强制为字符串,因此obj[0]始终与obj['0']相同。
# keyof应用场景
- 获取对象所有属性的类型
type Person = {
id: number;
name: string;
age: number;
};
type P2 = Person[keyof Person];
//type P2 = string | number
let ff:P2 = 100
TIP
- Person['key'] 是
查询类型(Lookup Types), 可以获取到对应属性类型的类型; - Person[keyof Person]本质上是执行 Person['id' | 'name' | 'age'];
- 由于联合类型具有分布式的特性,Person['id' | 'name' | 'age'] 变成了 Person['id'] | Person['name'] | Person['age'];
- 最后得到的结果就是 number | string.
ts环境搭建 →