名义型类型 & 结构型类型
- 名义类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。
- 结构类型系统中,只对值所具有的结构进行类型检查。简而言之,要判断两个类型是否是兼容的,只需要看两个类型的结构是否兼容就可以了,不需要关心类型的名称是否相同。
interface Duck {
sayGa: boolean;
}
class Animal {
sayGa: boolean;
horn: boolean;
tail: boolean;
}
let d: Duck;
// OK, because of structural typing
d = new Animal();
在程序设计中,鸭子类型(英语:duck typing)是动态类型的一种风格。 在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由"当前方法和属性的集合"决定。
简而言之:「如果它走起路来像鸭子,叫起来也是鸭子,那么我们就可以认为它就是鸭子,即使它头上有犄角!」。
名义类型使用 🌰
// 名义类型 Nominal Typing
// 一个计算纬度的函数的类型
type calcByDimension = (dimension: TParamOf2D) => void
type TParamOf2D = { x: number; y: number }
type TParamOf3D = { x: number; y: number; z: number }
// 因为 TParamOf3D中存在 x y属性,所以能通过类型校验
const vector3D: TParamOf3D = { x: 1, y: 1, z: 1 }
const calc: calcByDimension = (a) => {}
calc(vector3D) // 并没有抛出错误
type TParamOf2D = { x: number; y: number } & { __type: '2d' }
type TParamOf3D = { x: number; y: number; z: number } & { __type: '3d' }
// 虽然存在 x y 属性,但是 __type属性的类型不一致导致无法通过类型校验
const vector3D: TParamOf3D = { x: 1, y: 1, z: 1, __type: '3d' }
const calc: calcByDimension = (a) => {}
// error:类型“TParamOf3D”的参数不能赋给类型“TParamOf2D”的参数。ts(2345)
calc(vector3D)
字符串索引类型
-
ts 支持 string 和 number 索引类型,允许同时使用这两种类型的索引
-
number 索引类型对应的 类型 必须是 string 索引类型对应类型的 子类型。
因为 js 规定: 对象的键名一律为字符串。使用索引获取对应的值时,非字符串的索引会被转化为字符串。number 索引签名一般用于数组,但是 js 中数组本身也是对象,所以数组的索引(下标)实际上也就是只能为字符串
// 可以看到symbol不受影响
interface IIndex {
[key: string]: string | number
[index: number]: number
[s: symbol]: number | symbol | string | boolean
}
// 不过就算 number 索引的值为子类型,两种索引依然不能重名,否则运行时会报错,除非子类型的数据结构和父类型是一致的。
interface Animal {
type: string
}
interface Dog extends Animal {
name: string
}
interface DogData {
[x: string]: Animal
[x: number]: Dog
}
let animal: Animal = { type: 'dog' }
let dog: Dog = { type: 'dog', name: 'hotDog' }
let corgi: DogData = {}
// Error: 类型 "Animal" 中缺少属性 "name",但类型 "Dog" 中需要该属性。ts(2741)
corgi['1'] = animal
corgi[1] = dog
extends
- 泛型约束
// 定义方法获取传入参数的length属性
function getLength<T>(arg: T) {
return arg.length;
}
getLength(true) // throw error: arr上不存在length属性
// -----------------------------------------------
interface IHasLength {
length: number;
}
// 利用 extends 关键字在声明泛型时约束泛型需要满足的条件
function getLength<T extends IHasLength>(arg: T) {
return arg.length;
}
const fn = (a: any,b:any,c:any) => {}
const res = getLength(fn) // res: 3
- 条件类型
// 如果 T 是 U 的子类型,那么结果为X,否则为Y
T extends U ? X : Y
// 简单的类型匹配
type TypeName<T> =
T extends boolean ? "boolean" :
T extends undefined ? "undefined" :
T extends Function ? "function" :
"object";
type T2 = TypeName<true>; // "boolean"
type T3 = TypeName<() => void>; // "function"
type T4 = TypeName<string[]>; // "object"
// 补充个isAny,T 后置
type IsAny<T> = 0 extends 1 & T ? true : false
- 分发
分发一定是需要在 extends 产生的类型条件判断中,并且是前置类型
只有联合类型才会产生分发
分发一定要满足所谓的裸类型中才会产生效果
type GetSomeType1<T extends string | number | [string]> = T extends string[]
? 'a'
: 'b';
// 可以简单理解为 T 是裸类型,而[T]不是
type GetSomeType2<T extends string | number | [string]> = [T] extends string[]
? 'a'
: 'b';
type res1 = GetSomeType1<number | [string]> // a | b
type res2 = GetSomeType2<[string] | number>; // b
// 位置不同,结果不同
// 官方高级类型
type Exclude<T, K> = T extends K ? never : T
type r1 = Exclude<'a' | 'b' | 'c' | 'd', 'a' | 'c'>
type Include<T, K> = T extends K ? T : never
type r2 = Include<'a' | 'b' | 'c' | 'd', 'a' | 'c'>
infer
可以简单理解为一个 类型的 变量,它只能在条件类型中使用并且判定结果为 true 时生效
// 内置高级工具类型:
type ReturnType<T> = T extends (args: any) => infer R ? R : never
type ParamType<T> = T extends (args: infer P) => any ? P : never
type fn = (a: number) => string
type rt = ReturnType<fn> // type rt = string
type pt = ParamType<fn> // type pt = number
infer 推断联合类型 🌰
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never
type T10 = Foo<{ a: string; b: string }> // T10类型为 string
type T11 = Foo<{ a: string; b: number }> // T11类型为 string | number
// 获取数组中所有元素的联合类型
type Arr2Union<T> = T extends (infer R)[] ? R : never
type Arr = [string, number, symbol]
type Union = Arr2Union<Arr> // Union 类型为 string | number | symbol
通过infer 获取字符串首字母并将其转为大写 🌰
type FirstLetter2Upcase<S extends string> = S extends `${infer F}${infer Rest}` ? `${Capitalize<F>}${Rest}` : never
由于在字符串中 infer 并不能像在数组中直接获取到最后一项,所以如果要想将 字符串中最后一个字符转为大写就需要借助泛型。具体见泛型一节中 将字符串中最后一个字符转为大写
never
使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。
// A.ts
export type TRole = 'Root' | 'Admin' | 'Default'
function handleValue(value: TRole) {
switch (value) {
case 'Root':
break
case 'Admin':
break
case 'Default':
break
default:
break
}
}
当在 A.ts 中的向 TRole 增加了‘Default 类型后,如果不在 B.ts 中增加对应的 Default 分支就会报错:
不能将类型“string”分配给类型“never”。
// B.ts
import { TRole } from './never'
function handleValue(value: TRole) {
switch (value) {
case 'Root':
break
case 'Admin':
break
default:
// 此处 value 的类型被收敛为 never
const neverCheck: never = value
break
}
}
使用 never 来收敛条件类型
type FnArguments<T> = T extends (...args: infer A) => any ? A : never
type FnReturnType<T> = T extends (...args: any) => infer R ? R : never
枚举
-
普通枚举(数字枚举、字符串枚举、异构枚举)
数字枚举的值会以第一个元素的值递增;数字枚举可以反向枚举;
// index.ts
enum numberEnum {
a = 2,
b,
c,
}
enum stringEnum {
a = 'a',
b = 'b',
c = 'c'
}
console.log('数字枚举', numberEnum); // { '2': 'a', '3': 'b', '4': 'c', a: 2, b: 3, c: 4 }
console.log('字符串枚举', stringEnum); // { a: 'a', b: 'b', c: 'c' }
console.log('正向枚举', numberEnum.b) // 3
console.log('反向枚举', numberEnum[3]); // b
// 异构枚举
enum anyEnum {
first = 1,
second = '2',
three = Math.random() // 计算成员
}
编译后的 js 文件
// index.js
var numberEnum = void 0;
(function (numberEnum) {
numberEnum[numberEnum["a"] = 2] = "a";
numberEnum[numberEnum["b"] = 3] = "b";
numberEnum[numberEnum["c"] = 4] = "c";
})(numberEnum || (numberEnum = {}));
var stringEnum = void 0;
(function (stringEnum) {
stringEnum["a"] = "a";
stringEnum["b"] = "b";
stringEnum["c"] = "c";
})(stringEnum || (stringEnum = {}));
-
常量枚举
常量枚举只能使用常量枚举表达式,并且不同于常规的枚举,它们在编译阶段会被删除。 常量枚举成员在使用的地方会被内联进来。 之所以可以这么做是因为,常量枚举不允许包含计算成员。
const enum constanceEnum {
a = 2,
b = Math.random(), //Error: const enum member initializers must be constant expressions.ts(2474)
}
// ts文件
// 换成常量枚举后
const enum constanceEnum {
a = 2,
b,
}
// js文件
function select(value) {
switch (value) {
case 2 /* constanceEnum.a */:
break;
case 3 /* constanceEnum.b */:
break;
default:
break;
}
}
断言
- 基本断言
const person = {};
// 直接添加属性,报错
person.name = 'cdb'; // error: 'name' 属性不存在于 ‘{}’
interface Person {
name: string;
}
const person = {} as Person; // ok
person.name = 'cdb';
- 非空断言
// 非空断言
let nonValueOrString: string | null | undefined;
nonValueOrString.split('') //error: nonValueOrString可能为 “null” 或“未定义”。ts(18049)
nonValueOrString!.split('') // ok
- 确定赋值断言
let x: number;
const sum = x.toString() // error: 在赋值前使用了变量“x”。ts(2454)
x = 2
// 下面 2 种都是OK的,相当于告诉ts, 代码运行到此处时 x 必定有值
let x!: number;
const sum = x
x = 2
let x: number;
const sum = x! // ok
x = 2
// 非空 & 复制断言的区别?
- 双重断言(慎用)
`一般用于父子类型的转换`
interface IAnimal {
legs: number
}
interface IDeer extends IAnimal {
deerChirp: boolean
}
interface IHorse extends IAnimal {
horseChirp: boolean
}
const normalDeer: IDeer = { legs: 4, deerChirp: true }
const normalHorse: IHorse = { legs: 4, horseChirp: true }
const dubDeer: IDeer = { legs: 4, deerChirp: true };
(dubDeer as IHorse).horseChirp = true; // error: 类型 "IDeer" 到类型 "IHorse" 的转换可能是错误的,因为两种类型不能充分重 叠。如果这是有意的,请先将表达式转换为 "unknown"。ts(2352)
(dubDeer as IAnimal as IHorse).horseChirp = true; // ok
-
常量断言
as const 是TypeScript 中的一个修饰符,用来修改类型推断的行为。当
as const修饰符用在变量声明或表达式的类型上时,它会强制 TypeScript 将变量或表达式的类型视为不可变的(immutable)。这意味着,如果你尝试对变量或表达式进行修改,TypeScript 会报错。
const obj = {
dx: 'huang',
xd: 'ou'
} as const
obj.xd = '111' // error: 无法为“xd”赋值,因为它是只读属性。ts(2540)
const arr = ['1', true, { o: 'o' }] as const
arr.push('1') // error: 类型“readonly ["1", true, { readonly o: "o"; }]”上不存在属性“push”。ts(2339)
arr[0] = 2 // error: 无法为“0”赋值,因为它是只读属性。ts(2540)
// 使用 as const 后, obj 和 arr 对应的类型分别如下:
const obj: {
readonly dx: "huang";
readonly xd: "ou";
}
const arr: readonly ["1", true, { readonly o: "o"; }]
readonly 和 as const 都能将类型声明为仅可读,而 as const 还能将类型转换成常量
interface Foo {
readonly a: {
b: number,
},
}
const f: Foo = {
a: { b: 1 },
};
f.a = { b: 2 }; // Error: 无法分配到 "a" ,因为它是只读属性
f.a.b = 2; // 这里则没问题
const bar = {
a: { b: 1 },
} as const;
bar.a = {b:2} // 无法为“a”赋值,因为它是只读属性。ts(2540)
bar.a.b = 2; // 无法为“b”赋值,因为它是只读属性。ts(2540)
类型谓词
类型保护有以下几种:
- typeof 适用于判断基本类型
- instanceof 适用于判断类
- in
- is
interface Bird {
fly: () => any
eat: () => any
}
interface Fish {
swim: () => any
eat: () => any
}
type BF = Fish | Bird
// 保护
function isFish(pet: BF): pet is Fish {
return (pet as Fish).swim !== undefined
}
function isBird(pet: BF) {
return (pet as Bird).fly !== undefined
}
function getPet(): BF {
return { eat: () => true, fly: () => true }
}
const pet = getPet()
// -------如何才能放心的在对应的分支里调用对应的方法?----------------
if ((pet as Bird).fly) {
pet.fly() // error: 类型“{ swim: () => any; eat: () => any; }”上不存在属性“fly”。ts(2339)
(pet as Bird).fly() // ok
}
if (isBird(pet)) {
pet.fly()
} else if (isFish(pet)) {
pet.swim()
}
变型
变型 是发生在父子类型之间的
- 集合论中,属性更少的是子集
- 类型系统中,属性更多的是子类型
父类型比子类型涵盖的范围更广; 子类型比父类型更具体
协变: 允许子类型变型为父类型
逆变: 允许父类型变型为子类型
双向协变: 协变 + 逆变
不变: 不允许变型(即两种类型没有父子关系)
// 基础例子
interface Animal {
eat: boolean;
}
interface Duck extends Animal{
sayGa: boolean;
}
协变
// 协变
let a: Animal = {eat: true};
let d: Duck = {eat: true, sayGa: true};
a = d // 兼容,这就是一个协变
d = a // error: 类型 "Animal" 中缺少属性 "sayGa",但类型 "Duck" 中需要该属性。ts(2741)
逆变
let visitAnimal = (animal: Animal): Duck => {
animal.eat = true;
return {
sayGa: true,
eat: true,
}
}
let visitDuck = (duck: Duck): Animal => {
duck.sayGa = true;
return {
eat: true,
}
}
// error: 不能将类型“(duck: Duck) => Animal”分配给类型“(animal: Animal) => Duck”。参数“duck”和“animal” 的类型不兼容。
visitAnimal = visitDuck
// 兼容, 逆变成功
visitDuck = visitAnimal
总结
// 抽象一下
type one = (duck: Duck) => Animal
type two = (animal: Animal) => Duck
type res1 = one extends two ? true : false // false
type res2 = two extends one ? true : false // true
// 函数参数是逆变:Animal 可以变型为 Duck,父类型 -> 子类型
// 函数返回值是协变:Duck 可以变型为 Animal,子类型 -> 父类型
但是其实 TS 里的函数参数是允许双向协变的
let needDuck = (duck: Duck) => {
duck.eat
if (Math.random() >= 0.5) {
duck.sayGa
}
}
let needAnimal = (animal: Animal) => {
animal.eat
}
// 逆变 的情况下
needAnimal = needDuck // 多的不能给少的, 我只需要一个eat,duck完全可以满足,为什么不行? 所以允许双向协变
needDuck = needAnimal // 少的可以给多的
为什么 ts 允许函数参数双向协变:因为 ts 是结构型类型语言,如果 Duck[] 可以赋值给 Animal[],那么就意味着Duck[]可以赋值给 Animal[] ,从而导致设计上就允许了双向协变,这是 ts 设计者为了维持结构化类型兼容的一种取舍。但毕竟双向协变是不安全的,
所以在 TS (2.6+) 后 ,你可以通过开启 strictFunctionTypes 或 strict 来修复这个问题。设置之后,函数参数就是逆变而不再是双向协变的了。
不变
type A = {
a: number
}
type B = {
b: string
}
let AA: A = {a: 1}
let BB: B = { b: '2' };
AA.b = 'str' // error: 类型“A”上不存在属性“b”。ts(2339)
// ***** 强行变型 ******
(AA as unknow as B).b = 'string'
(AA as any as B).b = 'string'
泛型
简单理解就是将类型抽象成了一个变量,只不过这个变量是类型
使用的地方: 接口、类、函数
泛型位置不同的区别
例子:实现一个 Foreach函数, 定义callback遍历方法 两种方式 应该采用哪一种?
// 第一种
type Callback = <T>(item: T) => void
// 第二种
type Callback<T> = (item: T) => void;
const MyForEach = <T>(arr: T[], callback: Callback) => {
for (let i = 0; i < arr.length - 1; i++) {
callback(arr[i])
}
};
forEach(['1', 2, 3, '4'], (item) => {});
应该用第二种,因为TS 是一种静态类型检测,并不会执行你的代码
解析嵌套泛型
// 定义几个普通的类型
type obj = {
name: 'name'
age: 23
}
type nestedObj = {
a: obj
b: '123'
c: () => string
}
type TTuple = ['string', 'number']
// 一个接收泛型的工具类型
type TToolType<S> = S extends 'string' ? obj : nestedObj
type res1 = TToolType<2>
// type res1 = {
// a: obj;
// b: '123';
// c: () => string;
// d: TTuple;
// }
type res2 = ExpandNestedGeneric<TToolType<2>>
// type res2 = {
// a: {
// name: 'name';
// age: 23;
// };
// b: '123';
// c: () => string;
// d: ["string", "number"];
// }
// 解析嵌套泛型工具类型
export type ExpandNestedGeneric<T> = T extends object
? T extends Function
? T
: T extends infer O // 不用infer也是可以的
? { [K in keyof O]: ExpandNestedGeneric<O[K]> }
: never
: T
将字符串中最后一个字符转为大写
type testString = 'hello, is me'
// first, 先反转字符串,再大写首字母,再反转一遍字符串
type ReverseStr<S extends string, resStr extends string = ''> = S extends `${infer FirstLetter}${infer RestLetter}` ? ReverseStr<RestLetter, `${FirstLetter}${resStr}`> : resStr
type UpcaseFirstLetter<S extends string> = S extends `${infer FirstLetter}${infer RestLetter}` ? `${Capitalize<FirstLetter>}${RestLetter}` : never
type res = ReverseStr<UpcaseFirstLetter<ReverseStr<testString>>>
// second, 字符串中不可以通过infer拿到最后一项,但是 [] 可以,所以先将每个字符都存进数组里,再数组中利用infer取得最后一项(即最后一个字符)并转为大写后,再把此时的数组转为字符串
type Str2Arr<S extends string, resArr extends any[] = []> = S extends `${infer FirstLetter}${infer RestLetter}` ? Str2Arr<RestLetter, [...resArr, FirstLetter]> : resArr
type Arr2Str<A extends any[], resStr extends string = ''> = A extends [infer First extends string, ...infer Rest] ? Arr2Str<Rest, `${resStr}${First}`> : resStr
type UpcaseLastItemInArr<A extends any[]> = A extends [...infer Head, infer LastItem extends string] ? Arr2Str<[...Head, Capitalize<LastItem>]> : never
type res2 = UpcaseLastItemInArr<Str2Arr<testString>>
什么时候用interface,什么时候用type ?
摘自 AntdPro > Typescript 一节
推荐任何时候都是用 type, type 使用起来更像一个变量,与 interface 相比,type 的特点如下:
- 表达功能更强大,不局限于 object/class/function
- 要扩展已有 type 需要创建新 type,不可以重名
- 支持更复杂的类型操作
基本上所有用 interface 表达的类型都有其等价的 type 表达。在实践的过程中,我们也发现了一种类型只能用 interface 表达,无法用 type 表达,那就是往函数上挂载属性。
但是实验后, interface 与 type 并没有区别 o_O!....
interface FuncWithAttachment {
(param: string): boolean;
someProperty: number;
}
const testFunc: FuncWithAttachment = {};
const result = testFunc('mike'); // 有类型提醒
testFunc.someProperty = 3; // 有类型提醒