Appearance
Typescript
typescript 的数据类型有哪些
- boolean(布尔类型)
- number(数字类型)
- string(字符串类型)
- array(数组类型)
let arr:string[] = ['12', '23'];
、let arr:Array<number> = [1, 2];
- tuple(元组类型):允许表示一个已知元素数量和类型的数组,
let tupleArr:[number, string, boolean] = [12, '34', true];
- enum(枚举类型)
enum Color {Red, Green, Blue}; let c: Color = Color.Green;
- any(任意类型)
- null 和 undefined 类型
- void 类型:用于标识方法返回值的类型,表示该方法没有返回值。
- unknown(描述不确定的变量):可以将任意类型的值赋值给 unknown,但 unknown 类型的值只能赋值给 unknown 或 any
- never 类型:never 是其他类型 (包括 null 和 undefined)的子类型,可以赋值给任何类型。但是没有类型是 never 的子类型,这意味着声明 never 的变量只能被 never 类型所赋值。
- object 对象类型
never 和 void 有什么区别?
void:
- void 表示函数没有返回值,或者说函数返回的是 undefined。
- 当一个函数没有显式指定返回值类型时,它的返回类型默认为 void。
- 不能对 void 类型的变量赋予除 undefined 以外的值。
never:
- never 表示函数永远不会正常返回,或者说函数会抛出异常或无限循环。
- 通常 never 类型用于表示永远不会执行完的函数或抛出异常的函数,或者在类型系统中表示不可能发生的情况。
- 可以将 never 类型赋值给任何其他类型,但是反过来不行。
说说你对 TypeScript 中枚举类型的理解?应用场景?
什么是枚举
枚举是一个被命名的常量集合,类似于 js 对象, 枚举的使用是通过 enum 关键字进行定义。多处定义相同的枚举是可以进行合并操作
它分为:
- 数字枚举
- 字符串枚举
- 异构枚举
数字枚举
当我们声明一个枚举类型是,虽然没有给它们赋值,但是它们的值其实是默认的数字类型,而且默认从 0 开始依次累加:
ts
enum Direction {
Up, // 值默认为 0
Down, // 值默认为 1
Left, // 值默认为 2
Right, // 值默认为 3
}
console.log(Direction.Up === 0) // true
console.log(Direction.Down === 1) // true
console.log(Direction.Left === 2) // true
console.log(Direction.Right === 3) // true
如果我们将第一个值进行赋值后,后面的值也会根据前一个值进行累加 1:
ts
enum Direction {
Up = 10,
Down, // 11
Left, // 12
Right, // 13
}
这种自增的特点会导致一个问题的出现,如下:
ts
enum Direction {
Up, // 0
Down, // 1
Left, // 2
Right = 2,
}
由上可以看出,Direction.Left
和Direction.Right
都等于 2,这就会导致在使用它们做判断时,两个的条件都一样
字符串枚举
ts
enum Direction {
Up = 'Up',
Down = 'Down',
Left = 'Left',
Right = 'Right',
}
如果设定了一个变量为字符串之后,后续的字段也需要赋值字符串,否则报错:
ts
enum Direction {
Up = 'UP',
Down, // error TS1061: Enum member must have initializer
Left, // error TS1061: Enum member must have initializer
Right, // error TS1061: Enum member must have initializer
}
异构枚举
即把数字枚举和字符串枚举结合起来混合起来使用
ts
enum Direction {
No = 0,
Yes = 'YES',
}
this 的问题
在 ts 中,必须显示的指定 this 的类型,不然就会报错。
ts
function say() {
console.log(this.name) // ts(2683) “this”隐式具有类型“any”,因为它没有类型注释
}
say()
// 在上述代码中,如果我们直接调用 say 函数,this 应该指向全局 window 或 global(Node 中)。
// 但是,在 strict 模式下的 TypeScript 中,它会提示 this 的类型是 any,此时就需要我们手动显式指定类型了。
在 TypeScript 中,我们只需要在函数的第一个参数中声明 this 指代的对象(即函数被调用的方式)即可,比如最简单的作为对象的方法的 this 指向。
显式注解函数中的 this 类型,它表面上占据了第一个形参的位置,但并不意味着函数真的多了一个参数,因为 TypeScript 转译为 JavaScript 后,“伪形参” this 会被抹掉。
ts
function say(this: Window, name: string) {
console.log(this.name)
}
// say('sky') // “void”类型的“this”上下文不可分配给“Window”类型的方法“this”,后面会解释
window.say = say
window.say('name')
const obj = { say, name: 'bb' }
obj.say('dd')
// 报错 The 'this' context of type '{ say: (this: Window, name: string) => void; name: string; }' is not assignable to method's 'this' of type 'Window'
ts
interface Person {
name: string
say(this: Person): void
}
const person: Person = {
name: 'captain',
say() {
console.log(this.name)
},
}
const fn = person.say
fn() // 报错 The 'this' context of type 'void' is not assignable to method's 'this' of type 'Person'
ts
class Component {
onClick(this: Component) {}
}
const component = new Component()
interface UI {
addClickListener(onClick: (this: void) => void): void
}
const ui: UI = {
addClickListener() {},
}
ui.addClickListener(component.onClick) // 报错
// 上面示例中,我们定义的 Component 类的 onClick 函数属性(方法)显式指定了 this 类型是 Component,
// 在第 15 行作为入参传递给 ui 的 addClickListener 方法中,它指定的 this 类型是 void,两个 this 类型不匹配。
函数重载
针对同一个函数,根据函数的不同参数,返回的结果不一样。ts 的函数重载 匹配的规则是从上到下。
ts
interface P1 {
name: string
}
interface P2 extends P1 {
age: number
}
function convert(x: P1): number
function convert(x: P2): string
function convert(x: P1 | P2): any {}
const x1 = convert({ name: '' } as P1) // => number
const x2 = convert({ name: '', age: 18 } as P2) // number
// 因为 P2 继承自 P1,所以类型为 P2 的参数会和类型为 P1 的参数一样匹配到第一个函数重载,此时 x1、x2 的返回值都是 number。
// 改进,只需要把 P2 提前
function convert(x: P2): string
function convert(x: P1): number
function convert(x: P1 | P2): any {}
const x1 = convert({ name: '' } as P1) // => number
const x2 = convert({ name: '', age: 18 } as P2) // => string
类型谓词(is)
在添加返回值类型的地方,通过“参数名 + is + 类型”的格式明确表明了参数的类型,进而引起类型缩小。
ts
function isString(s: unknown): s is string {
// 类型谓词
return typeof s === 'string'
}
function isNumber(n: number) {
return typeof n === 'number'
}
function operator(x: unknown) {
if (isString(x)) {
// ok x 类型缩小为 string
}
if (isNumber(x)) {
// ts(2345) unknown 不能赋值给 number
}
}
说说 TypeScript 中的接口
简单来讲,一个接口所描述的是一个对象相关的属性和方法。可以重复定义,它们的属性会叠加
只读属性
我们可以通过在属性名前面加 readonly 修饰符来标注。
需要注意的是:加上 readonly 只是在检测层面会报错,实际编译过后还是可以更改的。
ts
interface ReadOnlyProgramLanguage {
readonly name: string
}
let ReadOnlyTypeScript: ReadOnlyProgramLanguage = {
name: 'TypeScript',
}
/** ts(2540)错误,name 只读 */
ReadOnlyTypeScript.name = 'JavaScript'
可选属性
ts
interface OptionalProgramLanguage {
name: string
age?: () => number
}
let OptionalTypeScript: OptionalProgramLanguage = {
name: 'TypeScript',
} // ok
接口的索引签名
我们可以对接口的索引进行类型约束。索引名称的类型分为 string 和 number 两种,通过如下两个接口,我们可以用来描述索引是任意数字或任意字符串的对象。
ts
// 有两个接口
interface StringInterface {
[name: string]: string
}
interface NumberInterface {
[name: number]: number
}
let str: StringInterface = {
age: 'fdf',
sex: 'fd',
2: 'fd', // ok
}
let num: NumberInterface = {
1: 3,
2: 5,
0: 3,
age: 3, // 报错
}
// 注意:在上述示例中,数字作为索引时,它的类型既可以与数字兼容,也可以与字符串兼容,
// 这与 JavaScript 的行为一致。因此,使用 0 或 '0' 索引对象时,这两者等价。
注意:虽然属性可以与索引签名进行混用,但是属性的类型必须是对应的数字索引或字符串索引的类型的子集,否则会出现错误提示。
ts
interface StringInterface {
[name: string]: string
sex: string // ok // age: number // 报错,因为它必须是跟前面的索引类型 [name: string] : string 同样的,只能是 string 或 string 的子集(比如 any、null、undefined、never)
}
interface NumberInterface {
name: string
age: number
[name: number]: number
}
let str: StringInterface = {
age: 'd',
sex: 'fd',
2: 'fd',
}
let num: NumberInterface = {
name: 'f',
2: 5,
0: 3,
age: 3,
}
// 还需要注意的是 我们不能约束数字索引属性与字符串索引属性拥有截然不同的类型,具体示例如下:
interface NumberInterface {
[name: number]: number // 报错
[rank: string]: string
}
需改成如下同种类型
interface NumberInterface {
[name: number]: number
[rank: string]: number
}
接口的继承
ts
interface Animal {
type: string
}
interface Dog {
name: string
}
// 继承一个
interface Cat extends Animal {
age: number
}
// 继承多个
interface Cat extends Animal, Dog {
age: number
}
说说 TypeScript 中的类型别名
它不能重复定义同一个类型别名
ts
type LanguageType = {
name: string
age: () => number
}
type fn = () => string
type ag = string | number
针对接口类型无法覆盖的场景,比如组合类型、交叉类型,我们只能使用类型别名
ts
interface Animal {
type: string
}
/** 联合 */
type MixedType = string | number
/** 交叉 */
type IntersectionType = { id: number; name: string } & {
age: number
name: string
}
/** 提取接口属性类型 */
type AgeType = Animal['type']
type UA = 'px' | 'em' | 'rem'
type UB = 'vh' | 'em' | 'rem'
type Union = UA & UB
const a: Union = 'em'
联合操作符 | 和 交叉操作符的优先级 & ,& 操作符的优先级更高,不管出现的顺序,可以通过 ()进行调整优先级。
Interface 与 Type 的相同点与区别
相同点:
- 都可以描述一个对象和函数类型
- 都可以扩展,不过实现方式不一样, interface 是用 extends , type 是用 & 符号(其实就是交叉类型),它们两个可以互相扩展
不同点:
- type 还可以声明基本类型、联合类型、元组类型等。
- type 支持映射。
- type 可以使用 typeof 返回的值。
- interface 可以合并重复声明, type 不可以。
class 类
公共、私有与受保护的修饰符
- public 修饰的是在任何地方可见、公有的属性或方法
- private 修饰的是仅在同一类中可见、私有的属性或方法
- protected 修饰的是仅在类自身及子类中可见、受保护的属性或方法
- readonly 只读修饰符
ts
class Animal {
public type = 'animal'
private name = 'sky'
protected age = 23
public readonly sex = 'man'
}
类的存取器
通过存取器 getter 和 setter 截取对类成员的读写访问。
ts
class Animal {
type = 'animal'
get myName() {
return this.type
}
set myName(name: string) {
this.type = name
}
}
const ani = new Animal()
console.log(ani.myName) // animal
ani.myName = 'sky'
console.log(ani.myName) // sky
类的继承
ts
class Animal {
type = 'animal'
}
class Dog extends Animal {
name: string
constructor(name: string) {
super() // 如果不调用,会报错:派生类的构造函数必须包含 "super" 调用
this.name = name
}
}
抽象类
它是一种不能被实例化仅能被子类继承的特殊类。通过 abstract 关键字去声明一个抽象类,以及抽象类中的抽象属性、抽象方法。
它跟接口有点类似,可以使用抽象类定义派生类需要实现的属性和方法,有区别的一点为,可以定义派生类的默认属性和方法。接口则不行。
ts
abstract class Adder {
abstract x: number
abstract y: number
abstract add(): number
displayName = 'Adder'
addTwice(): number {
return (this.x + this.y) * 2
}
}
// 通过 abstract 声明的属性和方法,在派生类 NumAdder 中,都需要实现。
class NumAdder extends Adder {
x: number
y: number
constructor(x: number, y: number) {
super()
this.x = x
this.y = y
}
add(): number {
return this.x + this.y
}
}
const numAdder = new NumAdder(1, 2)
console.log(numAdder.displayName) // => "Adder"
console.log(numAdder.add()) // => 3
console.log(numAdder.addTwice()) // => 6
使用接口定义类
ts
interface IAdder {
x: number
y: number
add: () => number
}
class NumAdder implements IAdder {
x: number
y: number
constructor(x: number, y: number) {
this.x = x
this.y = y
}
add() {
return this.x + this.y
}
addTwice() {
return (this.x + this.y) * 2
}
}
泛型
泛型指的是类型参数化,即将原来具体的类型进行参数化,根据传入的类型来明确。
设计泛型的目的在于有效约束类型成员之间的关系,比如函数参数和返回值、类或者接口成员和方法之间的关系。
函数泛型
ts
function returnItem<T>(para: T): T {
return para
}
// 定义泛型的时候,可以一次定义多个类型参数,比如我们可以同时定义泛型 T 和 泛型 U:
function swap<T, U>(tuple: [T, U]): [U, T] {
return [tuple[1], tuple[0]]
}
swap([7, 'seven']) // ['seven', 7]
接口泛型
ts
interface ReturnItemFn<T> {
(para: T): T
}
const returnItem: ReturnItemFn<number> = (para) => para
类泛型
在类的定义中,我们还可以使用泛型用来约束构造函数、属性、方法的类型
ts
class Memory<S> {
store: S
constructor(store: S) {
this.store = store
}
set(store: S) {
this.store = store
}
get() {
return this.store
}
}
const numMemory = new Memory<number>(1) // <number> 可缺省
const getNumMemory = numMemory.get() // 类型是 number
numMemory.set(2) // 只能写入 number 类型
const strMemory = new Memory('') // 缺省 <string>
const getStrMemory = strMemory.get() // 类型是 string
strMemory.set('string') // 只能写入 string 类型
实用工具类型
TypeScript 提供了几种实用工具类型来促进常见的类型转换。这些实用程序是全局可用的。
Partial<Type>
。将传入的 type 类型的所有属性设置为可选的类型。并返回一个新类型。Required<Type>
。将传入的 type 类型的所有属性设置为必选的类型。并返回一个新类型。跟 Partial 正好相反Readonly<Type>
。将传入的 type 类型的所有属性设置为只读,并返回一个新类型。Pick<Type, Keys>
。从 type 中选取部分属性 keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。跟 Omit 相反。Omit<Type, Keys>
。从 type 中删除部分属性 keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。Record<Keys, Type>
。构造一个对象类型,它的属性键是 keys,属性值是 type 类型。可用于将一种类型的属性映射到另一种类型。Keys 可以为固定的值,也可以是一个联合类型,但是当为联合类型的时候,必须全部有其中的属性。
ts
/**
* Partial<Type>
* 将传入的 type 的所有属性设置为可选的类型。并返回一个新类型
* 源码 type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
*/
interface OldPart {
id: number
age: number
}
const todo1: Partial<OldPart> = {
id: 3,
}
/**
* Required<Type>
* 将传入的 type 的所有属性设置为必选的类型。并返回一个新类型。跟 Partial 正好相反
* 源码 type Required<T> = { [P in keyof T]-?: T[P]; }
*/
type OldRequired = {
id?: number
age?: number
}
const todo2: Required<OldRequired> = {
id: 2,
age: 3,
}
/**
* Readonly<Type>
* 将传入的 type 的所有属性设置为只读,并返回一个新类型。
* 源码 type Readonly<T> = { readonly [P in keyof T]: T[P]; }
*/
interface OldRead {
id: number
}
const todo3: Readonly<OldRead> = {
id: 3,
}
// todo3.id = 5 // 无法分配到 "id" ,因为它是只读属性。ts(2540)
/**
* Record<Keys, Type>
* 构造一个对象类型,它的属性键是 keys,属性值是 type 类型。可用于将一种类型的属性映射到另一种类型。
* Keys 可以为固定的值,也可以是一个联合类型,但是当为联合类型的时候,必须全部有其中的属性
* 源码 type Record<K extends string | number | symbol, T> = { [P in K]: T; }
*/
interface OldRecord {
id: number
age: number
}
type CatName = 'miffy' | 'boris' | 'mordred'
const todo4: Record<'name', OldRecord> = {
name: { id: 3, age: 5 },
}
// 为联合类型的时候,必须全部有其中的属性
const todo5: Record<CatName, OldRecord> = {
miffy: { id: 3, age: 5 },
boris: { id: 3, age: 5 },
mordred: { id: 3, age: 5 },
}
/**
* Pick<Type, Keys>
* 通过从 type 中选取部分属性keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。跟 Omit 相反
* 源码 type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
*/
interface OldPick {
id: number
age: number
desc: string
}
const todo6: Pick<OldPick, 'age'> = {
age: 3,
}
const todo7: Pick<OldPick, 'age' | 'desc'> = {
age: 3,
desc: 'dd',
}
/**
* Omit<Type, Keys>
* 从 type 中删除部分属性keys(字符串字面值或字符串字面值的联合)来构造类型,返回新的类型。
* 源码 type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }
*/
interface OldOmit {
id: number
age: number
desc: string
}
const todo8: Omit<OldOmit, 'age'> = {
id: 3,
desc: 'dd',
}
const todo9: Omit<OldOmit, 'age' | 'desc'> = {
id: 3,
}
TypeScript 命名空间
命名空间一个最明确的目的就是解决重名问题,TypeScript 中命名空间使用 namespace 来定义。
命名空间定义了标识符的可见范围,一个标识符可在多个名字空间中定义,它在不同名字空间中的含义是互不相干的
ts
namespace SomeNameSpaceName {
export interface ISomeInterfaceName {}
export class SomeClassName {}
}
namespace Letter {
export let a = 1
export let b = 2
export let c = 3
export let z = 26
}
// 使用方式
SomeNameSpaceName.SomeClassName
TypeScript 装饰器
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上,是一种在不改变原类和使用继承的情况下,动态地扩展对象功能
类装饰
例如声明一个函数 addAge 去给 Class 的属性 age 添加年龄.
ts
function addAge(constructor: Function) {
constructor.prototype.age = 18
}
@addAge
class Person {
name: string
age!: number
constructor() {
this.name = 'huihui'
}
}
let person = new Person()
console.log(person.age) // 18
// 上述代码,实际等同于以下形式:
Person = addAge(function Person() { ... });
方法/属性装饰
装饰器可以用于修饰类的方法,这时候装饰器函数接收的参数变成了:
- target:对象的原型
- propertyKey:方法的名称
- descriptor:方法的属性描述符
ts
// 声明装饰器修饰方法/属性
function method(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log(target)
console.log('prop ' + propertyKey)
console.log('desc ' + JSON.stringify(descriptor) + '\n\n')
descriptor.writable = false
}
function property(target: any, propertyKey: string) {
console.log('target', target)
console.log('propertyKey', propertyKey)
}
class Person {
@property
name: string
constructor() {
this.name = 'huihui'
}
@method
say() {
return 'instance method'
}
@method
static run() {
return 'static method'
}
}
const xmz = new Person()
// 修改实例方法say
xmz.say = function () {
return 'edit'
}
参数装饰
接收 3 个参数,分别是:
- target :当前对象的原型
- propertyKey :参数的名称
- index:参数数组中的位置
ts
function logParameter(target: Object, propertyName: string, index: number) {
console.log(target)
console.log(propertyName)
console.log(index)
}
class Employee {
greet(@logParameter message: string): string {
return `hello ${message}`
}
}
const emp = new Employee()
emp.greet('hello')
装饰器工厂
如果想要传递参数,使装饰器变成类似工厂函数,只需要在装饰器函数内部再函数一个函数即可
ts
function addAge(age: number) {
return function (constructor: Function) {
constructor.prototype.age = age
}
}
@addAge(10)
class Person {
name: string
age!: number
constructor() {
this.name = 'huihui'
}
}
let person = new Person()
当多个装饰器应用于一个声明上,将由上至下依次对装饰器表达式求值,求值的结果会被当作函数,由下至上依次调用
ts
function f() {
console.log('f(): evaluated')
return function (
target,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('f(): called')
}
}
function g() {
console.log('g(): evaluated')
return function (
target,
propertyKey: string,
descriptor: PropertyDescriptor
) {
console.log('g(): called')
}
}
class C {
@f()
@g()
method() {}
}
// 输出
f() // evaluated
g() // evaluated
g() // called
f() // called