「这是我参与2022首次更文挑战的第32天,活动详情查看:2022首次更文挑战」
一、关于 TypeScript
JavaScript的超集,添加了静态类型检查和一些尚未正式发布的ECMAScript特性,最终会被编译成JavaScript执行。
二、基础
1.基础类型
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
boolean、number、string、void、undefined、null、symbol、bigint、any、unknown、never、数组、元组、对象、枚举。
enum Fruits {
apple = 1,
watermelon = 2,
peach = 3
}
(枚举类型能更加清晰地描述常量)
2.接口
type与interface的区别
-
type确实不能使用extends语法,但是type可以通过&(交叉类型)来达到同样的效果。
-
用联合类型生成的type,不能被interface extends xxx,也不能使用class来implements。
-
type不会声明合并。
-
interface内部不能使用映射类型语法。
interface A {
a1: number;
}
interface B extends A { }
type A1 = number;
// 报错
type B1 extends A1 {}
3.函数
4.原理
现在我们项目中的ts代码会经过babel编译,与tsc相比,babel不做类型检查,会直接去掉类型信息。因此在部分语法上有细微的区别。
-
const enum:const enum 是在编译期间把 enum 的引用替换成具体的值,需要解析类型信息,而 babel 并不会解析,所以不支持。
-
不支持 namespace 的跨文件合并,不支持导出非 const 的值。
三、进阶
1.泛型
官方介绍:软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。在像C#和Java这样的语言中,可以使用泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以在自己的数据类型来使用组件。
实际用途:增加类型定义的复用性。
// js
function Func(arg) {
return arg;
}
// 使用基础类型
function Func(arg: any): any {
return arg;
}
// 泛型
function Func<T>(arg: T): T {
return arg;
}
type Func = <T>(arg: T) => T;
2.联合类型 & 交叉类型
- 联合类型(Union Types) 联合类型是将变量命名为多个可能类型。
interface Brid {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Brid {
return
}
let pet = getSmallPet();
// 类型“Brid | Fish”上不存在属性“swim”。
// 类型“Brid”上不存在属性“swim”
pet.swim();
pet.layEggs();
联合类型使用时,如果需要调用变量的属性或方法,必须是联合的多个类型中共有的方法,否则会报错。
- 交叉类型(Intersection Types)
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如,
Person & Serializable & Loggable
同时是Person
和Serializable
和Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
function extend<T, U>(first: T, second: U): T & U {
let result = <T & U>{}
for (let id in first) {
(<any>result)[id] = (<any>first)[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<any>result)[id] = (<any>second)[id];
}
}
return result
}
class AdvancedTypesClass {
constructor(public name: string) { }
}
interface LoggerInterface {
log(): void;
}
class AdvancedTypesLoggerClass implements LoggerInterface {
log(): void {
console.log('console logging');
}
}
var logger = new AdvancedTypesLoggerClass();
var extend1 = extend(new AdvancedTypesClass("string"), new AdvancedTypesLoggerClass());
var e = extend1.name;
console.log(e); // string
extend1.log(); // console logging
3.类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。
语法:Value as Type
或 <Type>Value
。在tsx中必须使用前者。
用途:
-
将一个联合类型断言为其中一个类型
interface Cat { name: string; run(): void; } interface Fish { name: string; swim(): void; } // error function isFish(animal: Cat | Fish) { if (typeof (animal as Fish).swim === 'function') { return true; } return false; } // 编译不会报错,运行会报错 function swim(animal: Cat | Fish) { (animal as Fish).swim(); } const tom: Cat = { name: 'Tom', run() { console.log('run') } }; swim(tom); // right function isFish(animal: Cat | Fish) { if (typeof (animal as Fish).swim === 'function') { return true; } return false; }
-
继承断言
class ApiError extends Error { code: number = 0; } class HttpError extends Error { statusCode: number = 200; } function isApiError(error: Error) { if (typeof (error as ApiError).code === 'number') { return true; } return false; }
-
断言为any
(window as any).foo = 1;
-
断言为具体类型
function getCacheData(key: string): any { return (window as any).cache[key]; } interface Cat { name: string; run(): void; } const tom = getCacheData('tom') as Cat; tom.run();
断言原则:
- 联合类型可以被断言为其中一个类型
- 父类可以被断言为子类
- 任何类型都可以被断言为 any
- any 可以被断言为任何类型
- 要使得
A
能够被断言为B
,只需要A
兼容B
或B
兼容A
即可
使用泛型
function getCacheData<T>(key: string): T {
return (window as any).cache[key];
}
interface Cat {
name: string;
run(): void;
}
const tom = getCacheData<Cat>('tom');
tom.run();
4.类型保护
TypeScript里有类型保护机制。要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个类型谓词:
function isString(test: any): boolean {
return typeof test === "string";
}
function isString(test: any): test is string {
return typeof test === "string";
}
function example(foo: any) {
if (isString(foo)) {
console.log("it is a string" + foo);
console.log(foo.length); // string function
// 如下代码编译时会出错,运行时也会出错,因为 foo 是 string 不存在toExponential方法
console.log(foo.toExponential(2));
}
// 编译不会出错,但是运行时出错
console.log(foo.toExponential(2));
}
example("hello world");
5.命名空间
命名空间是多个命名的容器。
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
let strings = ["Hello", "98052", "101"];
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
for (let s of strings) {
for (let name in validators) {
console.log(`"${s}" - ${validators[name].isAcceptable(s) ? "matches" : "does not match"} ${name}`);
}
}
6.装饰器
官方介绍:装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用 @expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
实际用途:减少高阶组件的嵌套、增加函数的复用性。
- 装饰器工厂:声明一个装饰器工厂函数,返回一个表达式。
function color(value: string) { // 这是一个装饰器工厂
return function (target) { // 这是装饰器
// do something with "target" and "value"...
}
}
装饰器组合:多个装饰器应用在同一个声明上
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
类中不同声明上的装饰器将按以下规定的顺序应用:
-
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
-
参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
-
参数装饰器应用到构造函数。
-
类装饰器应用到类。
-
类装饰器
类装饰器修改类属性
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}
类装饰器重载构造函数
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}
@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}
类装饰器替换目标类
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。
interface IProps { }
export function withHeader(str: string = 'header') {
return function (Cmp: typeof React.Component): any {
return function (props: IProps) {
return (
<div>
<div className= "header" > { str } < /div>
< Cmp {...props } />
< /div>
);
};
};
}
@withHeader('测试')
export class GetQualityOrder extends Component {
constructor(props: IProps) {
super(props);
}
render() {
return <div>content < /div>;
}
}
- 类方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。方法装饰器不能用在声明文件( .d.ts
),重载或者任何外部上下文(比如declare
的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
- 成员的属性描述符。
function enumerable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}
- 访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如
declare
的类)里。
class Myclass {
private _a: number;
private _b: number;
constructor(a: number, b: number) {
this._a = a;
this._b = b;
}
@configurable(true) // 可以修改和删除
get a() { return this._a; }
@configurable(false) // 不可以修改和删除
get b() { return this._b; }
set a(num: number) {
this._a = num;
}
set b(num: number) {
this._b = num;
}
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
console.log(delete Myclass.prototype.a); // true
console.log(delete Myclass.prototype.b); // false
TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
不允许同时修改get和set
// wrong
class Myclass {
private _a: number;
constructor(a: number) {
this._a = a;
}
@configurable(true) // a 的 Getter先出现,则装饰器写于此
get a() { return this._a; }
@configurable(true) // 错误写法
set a(num: number) {
this._a = num;
}
}
// right
class Myclass {
private _a: number;
constructor(a: number) {
this._a = a;
}
@configurable(true) // a 的 Getter先出现,则装饰器写于此
get a() { return this._a; }
set a(num: number) { // 此处无需再用装饰器装饰
this._a = num;
}
}
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}
-
属性装饰器 属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如
declare
的类)里。属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
- 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
- 成员的名字。
实例成员
class MyClass {
constructor() {
}
@methodDecoratorName
myMethod() {
return "I am method named myMethod"
}
}
function methodDecoratorName(target, propertyKey, descriptor) {
console.log('target:', target); // target指向方法所属类的原型
console.log('target.myMethod():', target.myMethod()); // 可以在这使用该方法
console.log('propertyKey:', propertyKey); // 该方法的方法名
console.log('descriptor:', descriptor); // 该方法的属性描述符
}
let cls = new MyClass()
// target: { myMethod: [Function (anonymous)] }
// target.myMethod(): I am method named myMethod
// propertyKey: myMethod
// descriptor: {
// value: [Function (anonymous)],
// writable: true,
// enumerable: true,
// configurable: true
// }
静态成员
class MyClass {
constructor() {
}
@methodDecoratorName
static myMethod() {
return "I am a static member"
}
}
function methodDecoratorName(target, propertyKey, descriptor) {
console.log('target:', target);
console.log('target.myMethod():', target.myMethod());
console.log('propertyKey:', propertyKey);
console.log('descriptor:', descriptor);
}
let cls = new MyClass()
// target: [Function: MyClass] { myMethod: [Function (anonymous)] }
// target.myMethod(): I am a static member
// propertyKey: myMethod
// descriptor: {
// value: [Function (anonymous)],
// writable: true,
// enumerable: true,
// configurable: true
// }
底层
实例对象传入的target为MyClass.property,静态对象传入的target为MyClass
// 实例对象
var MyClass = /** @class */ (function () {
function MyClass() {
}
MyClass.prototype.myMethod = function () {
return "I am a static member";
};
__decorate([
methodDecoratorName
], MyClass.prototype, "myMethod", null);
return MyClass;
}());
// 静态对象
var MyClass = /** @class */ (function () {
function MyClass() {
}
MyClass.myMethod = function () {
return "I am a static member";
};
__decorate([
methodDecoratorName
], MyClass, "myMethod", null);
return MyClass;
}());
四、高级
类型推导
基础类型推导
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。
let a = 3;
// let a: number
function hello(person: string) {
return "Hello, " + person
}
// function hello(person: string): string
联合类型推导
如果有多个类型将联合推导
let x = [0, 1, null];
// let x: (number | null)[]
没有类型能共用时
let arr = [
{ name: "peter", type: "student" },
{ name: "tom", age: 18 },
];
/*
let arr: ({
name: string;
type: string;
age?: undefined;
} | {
name: string;
age: number;
type?: undefined;
})[]
*/
这种时候类型推导不一定能够得到我们想要结果,就需要我们明确指出类型:
interface Arr {
name: string;
type?: string;
age?: number;
}
let arr: Arr[] = [
{ name: "peter", type: "student" },
{ name: "tom", age: 17 },
];
上下文类型
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。
let a = 3;
a = ""; // Error: 不能将类型“string”分配给类型“number”。
function test(value: string) {
console.log(value);
}
test(1); // Error: 类型“number”的参数不能赋给类型“string”的参数。
typeof 操作符
在 TypeScript 中, typeof 操作符可以用来获取一个变量或对象的类型。
const sem = { name: "semlinker", age: 30 };
type Sem = typeof sem;
/*
type Sem = {
name: string;
age: number;
}
*/
const 断言
TypeScript 3.4 引入了一种新的字面量构造方式,也称为 const 断言。
let foo = {
name: "foo" as const,
contents: "contents",
};
/*
let foo: {
name: "foo";
contents: string;
}
*/
let goo = {
name: "goo",
contents: "contents",
} as const;
/*
let goo: {
readonly name: "goo";
readonly contents: "contents";
}
*/
高级类型
可索引类型
interface SomeArray {
[index: number]: any;
}
const someArray: SomeArray = [1, 2, "123", true,];
interface SomeObject {
[index: string]: any;
}
const someObject: SomeObject = {
name: "peter",
age: 18,
//...
};
索引类型
使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。
function pick(o, names) {
return names.map((n) => o[n]);
}
// function pick(o: any, names: any): any
- extends
有时候我们定义的泛型不想过于灵活或者说想继承某些类等,可以通过 extends
关键字添加泛型约束。也可理解为赋值。
interface TestExtends {
name: string;
age: number;
}
function testExtends<T extends TestExtends>(obj: T) {
console.log(obj.age);
console.log(obj.name);
}
testExtends({ name: "taotao", age: 18, type: "person" });
testExtends({ age: 18 }); // Error 类型 "{ age: number; }" 中缺少属性 "name",但类型 "TestExtends" 中需要该属性。
- keyof
keyof
操作符可以用来一个对象中的所有 key 值, 类似于 Object.keys()
const obj = {
name: "liqi",
age: 18,
type: "person",
};
type K1 = keyof typeof obj; // type K1 = "name" | "age" | "type"
/*
typeof obj = {
name: string;
age: number;
type: string;
}
keyof typeof obj = "name" | "age" | "type"
*/
我们再回来看例子
function pick(o, names) {
return names.map((n) => o[n]);
}
- 第一个参数
o
泛型T
- 第二个参数
names
是一个由参数o
的属性组成的数组,声明泛型K
,类型为K extends keyof T
,names
的类型为K[]
- 返回值我们通过类型访问符
T[K]
便可以取得对应属性值的类型,他们的数组T[K][]
正是返回值的类型。
function pick<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map((n) => o[n]);
}
const testPick = {
name: "taotao",
age: 18,
};
const result1 = pick(testPick, ["age", "name"]);
// const result1: (string | number)[]
const result2 = pick([], ["map"]);
// const result2: (<U>(callbackfn: (value: any, index: number, array: any[]) => U, thisArg?: any) => U[])[]
映射类型
我们有一个 User
接口,现在有一个需求是把 User
接口中的成员全部变成可选的,我们应该怎么做?难道要重新一个个 :
前面加上 ?
interface User {
username: string
id: number
token: string
avatar: string
role: string
}
这个时候映射类型就派上用场了,映射类型的语法是 [K in Keys]
in
关键字,类似于 for .. in
type Keys = "option1" | "option2";
type Flags = { [K in Keys]: boolean };
/*
type Flags = {
option1: boolean;
option2: boolean;
}
*/
然后我们需要将 keyof T
的属性名称一一映射出来 [K in keyof T]
,最后加上结果
type partial<T> = { [K in keyof T]?: T[K] };
条件类型
条件类型够表示非统一的类型,以一个条件表达式进行类型关系检测,从而在两种类型中选择其一:
T extends U ? X : Y
类似于JavaScript中的三元条件运算符
type Filter<T, U> = T extends U ? T : never;
type R1 = Filter<string | number | (() => void), Function>;
// type R1 = () => void
type TestFunction = {
name: string;
age: number;
getName: () => void;
};
type FindPropertyFunction<T> = {
[K in keyof T]: T[K] extends Function ? T[K] : never;
};
type Test = FindPropertyFunction<TestFunction>;
/*
type Test = {
name: never;
age: never;
getName: () => void;
}
*/
/*
进阶
type Test = {
getName: () => void;
}
*/
never
类型表示不会是任何值,即什么都没有,甚至不是null
类型
infer关键字
infer
是工具类型和底层库中非常常用的关键字,表示在 extends
条件语句中待推断的类型变量,简单来说可以用 infer
声明一个类型变量并且对它进行使用。
type func = (data: string, num: number) => void
type ParamType<T> = T extends (...param: infer P) => any ? P : T;
type Z = ParamType<func>
// type Z = [data: string, num: number]
type Foo = () => {
id: number;
name: string;
form?: string;
};
type ReturnTypes<T> = T extends () => infer P ? P : T;
type R1 = ReturnTypes<Foo>;
/*
type R1 = {
id: number;
name: string;
form?: string | undefined;
}
*/
const List = [
{ name: "liqi", age: 18 },
{ name: "peter", age: 18 },
{ name: "tom", age: 18 },
];
type ArrayType = typeof List;
type ArrayItemType<T> = T extends Array<infer P> ? P : T;
type Item = ArrayItemType<ArrayType>;
// type Item = { name: string; age: number };
常用工具类型
用 JavaScript 编写中大型程序是离不开 lodash 这种工具集的,而用 TypeScript 编程同样离不开类型工具的帮助,类型工具就是类型版的 lodash。
Partial
type Partial<T> = { [U in keyof T]?: T[U] };
DeepPartial*
type DeepPartial<T> = {
[U in keyof T]?: T[U] extends object
? DeepPartial<T[U]>
: T[U]
};
Required
type Required<T> = { [P in keyof T]-?: T[P] };
ReadOnly*
type ReadOnly<T> = { readonly [P in keyof T]: T[P] }
Mutable
type Mutable<T> = { -readonly [P in keyof T]: T[P] }
Exclude
Exclude
的作用是从 T
中排除出可分配给 U的元素.
type Exclude<T, U> = T extends U ? never : T;
type T = Exclude<1 | 2, 1 | 3> // -> 2
Record
type Records<T extends string, U> = {
[P in T]: U
}
interface PageInfo {
title: string;
}
type Page = "home" | "about" | "contact";
type nav = Records<Page, PageInfo>;
/*
type nav = {
home: PageInfo;
about: PageInfo;
contact: PageInfo;
}
*/
Pick
type pick<T, U extends keyof T> = {
[K in U]: T[K]
}
type A = {
a: number;
b: number;
c: number;
}
type X = pick<A, "a" | "b">;
// type X = { a: number; b: number; }
type TestFunction = {
name: string;
age: number;
getName: () => void;
};
type FindPropertyFunction<T> = {
[K in keyof T]: T[K] extends Function ? T[K] : never;
};
type Test = FindPropertyFunction<TestFunction>;
/*
type Test = {
name: never;
age: never;
getName: () => void;
}
*/
type FindFunctionProperty<T> = {
[K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type Value = FindFunctionProperty<TestFunction>
// type Value = "getName"
type pick<T, U extends keyof T> = {
[K in U]: T[K]
}
type C = pick<TestFunction, Value>
/*
type Test = {
getName: () => void;
}
*/
Omit
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
type Foo = Omit<{ name: string, age: number }, 'name'>
// -> { age: number }
当然如果你想进一步学习类型工具的设计,建议阅读 utility-types
的源码
五、应用
is 关键字
const arr = [12, null, undefined, 7, 13, "12"];
const list1 = arr.filter((item) => !!item);
// const list1: (number | null | undefined)[]
const list2 = arr.filter((item): item is number => !!item);
// const list2: number[]
可调用类型注解
interface KPhone {
new(): string;
}
declare const a: KPhone;
const b = new a();
// const b: string
双重断言
const a: string = '';
const b: number = a; // Error
const c: number = a as number; // Error
const d: number = a as any as number;