Typescript常看常新

JavaScript/前端
220
0
0
2024-04-24
标签   TypeScript

在看了同事推荐的ts教程后,发现自己还是有很多不会的,所以整理出一些自己学到的新知识点,希望各位也能有所收获!(我就写给自己看看,不要太当回事哈哈哈

附上教程链接,看完你就是ts大神!!!写得超级详细,保证有很多你不知道的小细节。 函数

如果变量被赋值为一个函数,变量的类型有两种写法。

代码语言:javascript

复制

// 写法一
const hello = function (txt: string) {
  console.log("hello " + txt);
};

// 写法二
const hello: (txt: string) => void = function (txt) {
  console.log("hello " + txt);
};

普通函数的写法:

代码语言:javascript

复制

function doSomething(f: Function) {
  return f(1, 2, 3);
}

箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。

代码语言:javascript

复制

const repeat = (str: string, times: number): string => str.repeat(times);

对象

关于只读属性:

属性名前面加上readonly关键字,表示这个属性是只读属性,不能修改。

代码语言:javascript

复制

interface MyInterface {
  readonly prop: number;
}

注意,如果属性值是一个对象,readonly修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。

另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量。

如果希望属性值是只读的,除了声明时加上readonly关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言as const。

代码语言:javascript

复制

const myUser = {
  name: "Sabrina",
} as const;

myUser.name = "Cynthia"; // 报错

对象解构赋值的写法,和声明对象类型是一样的

代码语言:javascript

复制

const {id, name, price}: {
  id: string;
  name: string;
  price: number;
} = product;

注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途。

代码语言:javascript

复制

let { x: foo, y: bar } = obj;

// 等同于
let foo = obj.x;
let bar = obj.y;

空对象

空对象是 TypeScript 的一种特殊值,也是一种特殊类型。

代码语言:javascript

复制

const obj = {};
obj.prop = 123; // 报错

上面示例中,变量obj的值是一个空对象,然后对obj.prop赋值就会报错。

原因是这时 TypeScript 会推断变量obj的类型为空对象,实际执行的是下面的代码。

代码语言:javascript

复制

const obj: {} = {};

这种写法其实在 JavaScript 很常见:先声明一个空对象,然后向空对象添加属性。但是,TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。

代码语言:javascript

复制

// 错误
const pt = {};
pt.x = 3;
pt.y = 4;

// 正确
const pt = {
  x: 3,
  y: 4,
};

如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(...)合成一个新对象。

代码语言:javascript

复制

const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };

const pt = {
  ...pt0,
  ...pt1,
  ...pt2,
};

interface 可以表示对象的各种语法,它的成员有 5 种形式。

  • 对象属性
  • 对象的属性索引
  • 对象方法
  • 函数
  • 构造函数

对象的方法共有三种写法。

代码语言:javascript

复制

// 写法一
interface A {
  f(x: boolean): string;
}

// 写法二
interface B {
  f: (x: boolean) => string;
}

// 写法三
interface C {
  f: { (x: boolean): string };
}

interface 也可以用来声明独立的函数

代码语言:javascript

复制

interface Add {
  (x: number, y: number): number;
}

const myAdd: Add = (x, y) => x + y;

interface

interface 允许多重继承。

代码语言:javascript

复制

interface Style {
  color: string;
}

interface Shape {
  name: string;
}

interface Circle extends Style, Shape {
  radius: number;
}

多重接口继承,实际上相当于多个父接口的合并。因此,定义的对象必须满足里面的所有属性和值。

interface和type的区别

interface 与 type 的区别有下面几点。

(1)type能够表示非对象类型,而interface只能表示对象类型(包括数组、函数等)。

(2)interface可以继承其他类型,type不支持继承。

继承的主要作用是添加属性,type定义的对象类型如果想要添加属性,只能使用&运算符,重新定义一个类型。

(3)同名interface会自动合并,同名type则会报错。也就是说,TypeScript 不允许使用type多次定义同一个类型。

(4)interface不能包含属性映射(mapping),type可以。

代码语言:javascript

复制

interface Point {
  x: number;
  y: number;
}

// 正确
type PointCopy1 = {
  [Key in keyof Point]: Point[Key];
};

// 报错
interface PointCopy2 {
  [Key in keyof Point]: Point[Key];
};

(5)type 可以扩展原始数据类型,interface 不行。

代码语言:javascript

复制

// 正确
type MyStr = string & {
  type: "new";
};

// 报错
interface MyStr extends string {
  type: "new";
}

(6)interface无法表达某些复杂类型(比如交叉类型&和联合类型|),但是type可以。

代码语言:javascript

复制

type A = {
  /* ... */
};
type B = {
  /* ... */
};

type AorB = A | B;
type AorBwithName = AorB & {
  name: string;
};

泛型

一般来说,泛型的类型可以自动推断,但是在复杂的情况下,ts无法推断类型参数的值,这个时候需要显式地给出。

代码语言:javascript

复制

function comb<T>(arr1: T[], arr2: T[]): T[] {
  return arr1.concat(arr2);
}
comb([1, 2], ["a", "b"]); // 报错

如上情况,是需要显式给出类型的。

代码语言:javascript

复制

comb<number | string>([1, 2], ["a", "b"]); // 正确

interface 也可以采用泛型的写法。

代码语言:javascript

复制

interface Box<Type> {
  contents: Type;
}

let box: Box<string>; // 需要给类型参数的值

泛型接口还有第二种写法。

代码语言:javascript

复制

interface Fn {
  <Type>(arg: Type): Type;
}

function id<Type>(arg: Type): Type {
  return arg;
}

let myId: Fn = id;

上面示例中,Fn的类型参数Type的具体类型,需要函数id在使用时提供。所以,最后一行的赋值语句不需要给出Type的具体类型。

此外,第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。

类型别名的泛型写法

type 命令定义的类型别名,也可以使用泛型。

代码语言:javascript

复制

type Nullable<T> = T | undefined | null;

上面示例中,Nullable<T>是一个泛型,只要传入一个类型,就可以得到这个类型与undefined和null的一个联合类型。

类型参数的默认值

类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。

代码语言:javascript

复制

function getFirst<T = string>(arr: T[]): T {
  return arr[0];
}

类型参数的约束条件

其实这个之前也讲过,定义一个函数,返回的是传入参数的长度length,但是我们知道不是所有类型都有length属性,所以此时我们可以使用泛型来约束传入参数的类型

代码语言:javascript

复制

interface Length {
    length:number
}

function getLength<T extends Length>(arg: T) {
    return arg.length
}

这样传入的参数的类型就被限制住了,只有拥有length属性的参数才能被传入函数中

类型参数的约束条件采用下面的形式。

代码语言:javascript

复制

<TypeParameter extends ConstraintType>

enum

基本形式

代码语言:javascript

复制

enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}

上面示例声明了一个 Enum 结构Color,里面包含三个成员Red、Green和Blue。第一个成员的值默认为整数0,第二个为1,第二个为2,以此类推。

使用

代码语言:javascript

复制

let c: Color = Color.Green; // 正确
let c: number = Color.Green; // 正确
注意不能这样写
let c: number = Color['GREEN'] // 错误

编译前后

代码语言:javascript

复制

// 编译前
enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}

// 编译后
let Color = {
  Red: 0,
  Green: 1,
  Blue: 2,
};

使用例子

代码语言:javascript

复制

enum Operator {
  ADD,
  DIV,
  MUL,
  SUB,
}

function compute(op: Operator, a: number, b: number) {
  switch (op) {
    case Operator.ADD:
      return a + b;
    case Operator.DIV:
      return a / b;
    case Operator.MUL:
      return a * b;
    case Operator.SUB:
      return a - b;
    default:
      throw new Error("wrong operator");
  }
}

compute(Operator.ADD, 1, 3); // 4

类型断言

类型断言也是开发中经常会用到的东西,这里补充一点知识吧

as

对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。

代码语言:javascript

复制

type T = "a" | "b" | "c";
let foo = "a";

let bar: T = foo; // 报错

在上面的例子中,ts将foo的类型推断为string, 而T是string的子类型,父类型不能赋值给子类型,所以会报错。此时就要用到类型断言,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。

代码语言:javascript

复制

type T = "a" | "b" | "c";

let foo = "a";
let bar: T = foo as T; // 正确

再看一个实际例子

代码语言:javascript

复制

const username = document.getElementById("username");

if (username) {
  (username as HTMLInputElement).value; // 正确
}

上面示例中,变量username的类型是HTMLElement | null,排除了null的情况以后,HTMLElement 类型是没有value属性的。如果username是一个输入框,那么就可以通过类型断言,将它的类型改成HTMLInputElement,就可以读取value属性。

注意,上例的类型断言的圆括号是必需的,否则username会被断言成HTMLInputElement.value,从而报错。

关于as const,有非常细节的知识

如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。

代码语言:javascript

复制

// 类型推断为基本类型 string
let s1 = "JavaScript";

// 类型推断为字符串 “JavaScript”
const s2 = "JavaScript";

有时候会导致奇怪的结果,看下例

代码语言:javascript

复制

let str = 'a' // 类型为string
const str1 = 'a'  //类型为'a' 
type lang = 'a' | 'b' | 'c'
function foo(language:lang) {return language}
foo(str) // 报错 str为父类型
foo(str1) //正确  他是联合类型的子类型

或者按照下面的修改,在声明时就做类型断言

代码语言:javascript

复制

let str = 'a' as const
const str1 = 'a'
type lang = 'a' | 'b' | 'c'
function foo(language:lang) {return language}
foo(str) // 正确

使用了as const断言以后,let 变量就不能再改变值了。因为相当于用const声明的

注意,as const断言只能用于字面量,不能用于变量

不能写成下面的样子:

代码语言:javascript

复制

let str = 'a' 
const str1 = 'a'
type lang = 'a' | 'b' | 'c'
function foo(language:lang) {return language}
foo(str as const) // 错误

非空断言

非空断言在实际编程中很有用,有时可以省去一些额外的判断

代码语言:javascript

复制

const root = document.getElementById("root");

// 报错
root.addEventListener("click", (e) => {
  /* ... */
});

上面示例中,getElementById()有可能返回空值null,即变量root可能为空,这时对它调用addEventListener()方法就会报错,通不过编译。但是,开发者如果可以确认root元素肯定会在网页中存在,这时就可以使用非空断言。

代码语言:javascript

复制

const root = document.getElementById("root")!;

运算符

终于到了运算符,这块用的比较少,学习一下!

keyof 运算符

keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型

注意,是联合类型!

代码语言:javascript

复制

type MyObj = {
  foo: number;
  bar: string;
};

type Keys = keyof MyObj; // 'foo'|'bar'

例子

代码语言:javascript

复制

type Obj = {
    name: string,
    age: number
}
type O = keyof Obj // 'name' | 'age'
let key: O = 'name'

下面是另一个例子。

代码语言:javascript

复制

interface T {
  0: boolean;
  a: string;
  b(): void;
}

type KeyT = keyof T; // 0 | 'a' | 'b'

由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是string|number|symbol。

代码语言:javascript

复制

// 示例一
interface T {
  [prop: number]: number;
}

// number
type KeyT = keyof T;

// 示例二
interface T {
  [prop: string]: number;
}

// string|number
type KeyT = keyof T;

上面的示例二,keyof T返回的类型是string|number,原因是 JavaScript 属性名为字符串时,包含了属性名为数值的情况,因为数值属性名会自动转为字符串。

对于联合类型,keyof 返回成员共有的键名。

代码语言:javascript

复制

type A = { a: string; z: boolean };
type B = { b: string; z: boolean };

// 返回 'z'
type KeyT = keyof (A | B);

对于交叉类型,keyof 返回所有键名。

代码语言:javascript

复制

type A = { a: string; x: boolean };
type B = { b: string; y: number };

// 返回 'a' | 'x' | 'b' | 'y'
type KeyT = keyof (A & B);

// 相当于
keyof (A & B) ≡ keyof A | keyof B

keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写

代码语言:javascript

复制

type MyObj = {
  foo: number;
  bar: string;
};

type Keys = keyof MyObj;

type Values = MyObj[Keys]; // number|string

keyof 运算符的用途

keyof 运算符往往用于精确表达对象的属性类型。

举例来说,取出对象的某个指定属性的值,JavaScript 版本可以写成下面这样。

代码语言:javascript

复制

function prop(obj, key) {
  return obj[key];
}

上面这个函数添加类型,只能写成下面这样。

代码语言:javascript

复制

function prop(obj: object, key: string): any {
  return obj[key];
}

上面的类型声明有两个问题,一是无法表示参数key与参数obj之间的关系,二是返回值类型只能写成any。

有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。

代码语言:javascript

复制

function prop<Obj, K extends keyof Obj>(
  obj:Obj, key:K
):Obj[K] {
  return obj[key];
}

keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。

代码语言:javascript

复制

type Obj = {
    name: string,
    age:number,
    gender: boolean
}
type O = {
    [prop in keyof Obj]: string
}

上面的例子中,将Obj中所有属性对应的值类型都修改为了string类型,变成了一个新的类型

in 运算符

TypeScript 语言的类型运算中,in运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。

代码语言:javascript

复制

type U = "a" | "b" | "c";

type Foo = {
  [Prop in U]: number;
};
// 等同于
type Foo = {
  a: number;
  b: number;
  c: number;
};

其实和上面的例子很像, keyof得出的结果也是一个联合类型

方括号运算符([])用于取出对象的键值类型,比如T[K]会返回对象T的属性K的类型。

代码语言:javascript

复制

type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// Age 的类型是 number
type Age = Person["age"];

方括号的参数如果是联合类型,那么返回的也是联合类型。

代码语言:javascript

复制

type Person = {
  age: number;
  name: string;
  alive: boolean;
};

// number|string
type T = Person["age" | "name"];

// number|string|boolean
type A = Person[keyof Person];

方括号运算符的参数也可以是属性名的索引类型。

代码语言:javascript

复制

type Obj = {
  [key: string]: number;
};

// number
type T = Obj[string];
// 同前面的例子, 也可以这么写
type T = Obj[number];
// 因为不管索引类型是字符串还是数字类型,最后属性都会被转为字符串
// 的形式, 因此Obj的索引类型中也包含了number

数组也可以

数组也是也对象嘛, 就是{0: 'a', 1: 'b', 2: 'c'}这样的形式,所以类型是{ [key:number]:string }

代码语言:javascript

复制

// MyArray 的类型是 { [key:number]:string }
const MyArray = ["a", "b", "c"];

// 等同于 (typeof MyArray)[number]
// 返回 string
type Person = (typeof MyArray)[number];

类型映射

为了增加代码复用性,可以把常用的映射写成泛型

代码语言:javascript

复制

type ToBoolean<Type> = {
  [Property in keyof Type]: boolean;
};

最后就到了高级类型工具啦, 就写几个常用的吧!

类型工具

Exclude<UnionType, ExcludedMembers>

Exclude<UnionType, ExcludedMembers>用来从联合类型UnionType里面,删除某些类型ExcludedMembers,组成一个新的类型返回。

注意这个的第一个参数是联合类型哈,不是整个的对象类型

代码语言:javascript

复制

type T1 = Exclude<"a" | "b" | "c", "a">; // 'b'|'c'
type T2 = Exclude<"a" | "b" | "c", "a" | "b">; // 'c'
type T3 = Exclude<string | (() => void), Function>; // string

注意这里Function是ts的一种内置函数类型。

与之对应的就是extract

本文由“壹伴编辑器”提供技术支持

Extract<Type, Union>

Extract<UnionType, Union>用来从联合类型UnionType之中,提取指定类型Union,组成一个新类型返回。它与Exclude<T, U>正好相反。

代码语言:javascript

复制

type T3 = Extract<"a" | "b" | "c", "a" | "d">; // 'a'
type T4 = Extract<string | string[], any[]>; // string[]
type T5 = Extract<(() => void) | null, Function>; // () => void

本文由“壹伴编辑器”提供技术支持

Omit<Type, Keys>

Omit<Type, Keys>用来从对象类型Type中,删除指定的属性Keys,组成一个新的对象类型返回。

代码语言:javascript

复制

interface A {
  x: number;
  y: number;
}

type T1 = Omit<A, "x">; // { y: number }
type T2 = Omit<A, "y">; // { x: number }
type T3 = Omit<A, "x" | "y">; // { }

这里的参数就是对象类型了,需要分清楚,与之对应的是pick

本文由“壹伴编辑器”提供技术支持

Pick<Type, Keys>

Pick<Type, Keys>返回一个新的对象类型,第一个参数Type是一个对象类型,第二个参数Keys是Type里面被选定的键名。

代码语言:javascript

复制

interface A {
  x: number;
  y: number;
}

type T1 = Pick<A, "x">; // { x: number }
type T2 = Pick<A, "y">; // { y: number }
type T3 = Pick<A, "x" | "y">; // { x: number; y: number }

指定的键名Keys必须是对象键名Type里面已经存在的键名,否则会报错。

Record<Keys, Type>

Record<Keys, Type>返回一个对象类型(注意是对象类型!),参数Keys用作键名,参数Type用作键值类型。

代码语言:javascript

复制

// { a: number }
type T = Record<"a", number>;

上面示例中,Record<Keys, Type>的第一个参数a,用作对象的键名,第二个参数number是a的键值类型。

因为第一个参数keys是键名,所以!一定是兼容 string|number|symbol,否则不能用作键名,会报错。