0%

readonly

字段可以添加一个readonly的前缀修改符,会阻止在构造函数之外的赋值

1
2
3
4
5
6
7
8
9
10
11
12
class Greeter {
readonly name:string="world";
constructor(otherName?:stirng){
if(otherName!==undefined){
this.name=otherName;
}
}
err(){
this.name="not ok";
// Cannot assign to 'name' because it is a read-only property.
}
}

类构造函数

可以使用带类型注解的参数,默认值,重载等,

1
2
3
4
5
6
7
8
class Point {
// Overloads
constructor(x: number, y: string);
constructor(s: string);
constructor(xs: any, y?: any) {
// TBD
}
}

类构造函数与函数签名的区别:

  • 构造函数不能有类型参数
  • 构造函数不能返回类型注解,因为总是返回类实例类型

Getter/Setter

1
2
3
4
5
6
7
8
9
class C {
_length = 0;
get length() {
return this._length;
}
set length(value) {
this._length = value;
}
}

规则:

  • 如果get存在而set不存在,属性自动设置为readonly
  • 如果setter参数的类型没有指定,它会被推断为getter的返回类型
  • getters和setters必须有相同的成员可见性

Ts4.3后,存取器的读取和设置可以使用不同的类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Thing {
_size=0;
//这里返回number类型
get size(): number {
return this._size;
}
//这里允许传入的是string|number|boolean类型
set size(value:string | number | boolean){
let num=Number(value);
if(!Number.isFinite(num)){
this._size=0;
return;
}
this._size=num;
}
}

覆写

派生类可以覆写一个基类的字段或者属性,可以使用super语法访问基类方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base {
greet() {
console.log("Hello, world!");
}
}

class Derived extends Base {
greet(name?: string) {
if (name === undefined) {
super.greet();
} else {
console.log(`Hello, ${name.toUpperCase()}`);
}
}
}

const d = new Derived();
d.greet();
d.greet("reader");

初始化顺序:

  • 基类字段初始化
  • 基类构造函数运行
  • 派生类字段初始化
  • 派生类构造函数运行

这意味着基类构造函数只能看到它自己的name的值,因为此时派生类字段初始化还没有运行

静态类:

静态类指的是作为类的静态成员存在于某个类的内部的类

静态类之所以存在时因为这些语言强迫所有数据和函数都要在一个类内部,但这个限制在TypeScript中不存在,所以没有静态类的需要,一个只有一个单独实例的类,在JavaScript/TypeScript中,完全可以使用普通对象替代。

举个例子,我们不需要一个static class语法,因为TypeScript中一个常规对象(或者顶级函数)可以实现一样功能

1
2
3
4
5
6
7
8
9
10
11
12
// Unnecessary "static" class
class MyStaticClass {
static doSomething() {}
}

// Preferred (alternative 1)
function doSomething() {}

// Preferred (alternative 2)
const MyHelperObject = {
dosomething() {},
};

泛型类:

类和接口一样,也可以写泛型,当使用new实例化一个泛型类,它的类型参数的推断和函数调用是同样的方式

1
2
3
4
5
6
7
8
class Box<Type> {
contents:Type;
constructor(value:Type){
this.contents=value;
}
}
const b=new Box("hello");
//const b:Box<stirng>

静态成员不允许引用类型参数:

1
2
3
4
class Box<Type>{
static defaultValue:Type;
//Static members cannot reference class type parameters
}

运行时只有一个Box.defaultValue属性槽,如果设置Box.defaultValue,就会改变Box.defaultValue,这样是不好的

使用箭头函数保留上下文

a.ts

1
2
3
4
5
6
7
8
9
10
class MyClass {
name = "MyClass";
getName = () => {
return this.name;
};
}
const c = new MyClass();
const g = c.getName;
// Prints "MyClass" instead of crashing
console.log(g());

转为a.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var MyClass = /** @class */ (function () {
function MyClass() {
var _this = this;
this.name = "MyClass";
this.getName = function () {
return _this.name;
};
}
return MyClass;
}());
var c = new MyClass();
c.getName();
var g = c.getName;
console.log(g());

注意:

  • this的值在运行时是正确的,即使TypeScript不检查代码
  • 这会使用更多的内存,因为每一个类实例都会拷贝一遍这个函数
  • 你不能在派生类使用super.getName,因为在原型链中并没有入口可以获取基类方法

this参数

在Ts方法或函数定义中,第一个参数且名字为this有特殊含义,该参数会在编译时被抹除Ts会检查一个有this参数的函数在调用时是否有一个正确的上下文,不用像使用箭头函数一样,我们可以给方法定义添加一个this参数,静态强制方法被正确调用

1
2
3
4
5
6
7
8
9
10
class MyClass {
name="MyClass";
getName(this:MyClass){
return this.name;
}
}
const c=new MyClass();
c.getName();
const g=c.getName;
console.log(g());//参数void不能赋值给参数MyClass

注意:和箭头函数相反

  • Js调用者依然可能在没有意识到它的时候错误使用类方法
  • 每个类一个函数,而不是每个类实例一个函数
  • 基类方法定义依然可以通过super调用

this类型

在类中,有一个特殊的名为this的类型,会动态的引用当前类的类型

1
2
3
4
5
6
7
8
class Box {
contents: string = "";
set(value: string) {
// (method) Box.set(value: string): this
this.contents = value;
return this;
}
}

这里,Ts推断set的返回类型为this而不是Box

1
2
3
4
5
6
7
8
9
10
class ClearableBox extends Box {
clear() {
this.contents = "";
}
}

const a = new ClearableBox();
const b = a.set("hello");

// const b: ClearableBox

下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Box {
content: string = "";
sameAs(other: this) {
return other.content === this.content;
}
}

class DerivedBox extends Box {
otherContent: string = "?";
}

const base = new Box();
const derived = new DerivedBox();
derived.sameAs(base);
// Argument of type 'Box' is not assignable to parameter of type 'DerivedBox'.
// Property 'otherContent' is missing in type 'Box' but required in type 'DerivedBox'.

other:this不同于other:Box,derived.sameAs方法值接受类置于同一个派生类的实例

基于this的类型保护:

在类和接口的方法的返回的位置,使用this is Type,当搭配使用类型收窄,目标对象的类型会被收窄为更具体的Type

例子:对一个特定字段进行懒校验,当hasValue被验证为true,会从类型中移除undefined

1
2
3
4
5
6
7
8
9
10
11
12
class Box<T>{
value?:T;
hasValue():this is {value:T}{
return this.value!==undefined;
}
}
const box=new Box();
box.value="Game";
box.value;// (property) Box<unknown>.value?: unknown
if(box.hasValue()){
box.value;// (property) value: unknown
}

总结:

构造函数中,this表示当前实例对象,在方法中,this表示当前调用方法的对象

参考:

https://ts.yayujs.com/handbook/Class.html#%E7%B1%BB%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E7%B3%BB-relationships-between-classes

https://www.typescriptlang.org/docs/handbook/2/classes.html

keyof

对一个对象使用keyof操作符,会返回该对象属性名组成的一个字符串或者数字字面量的联合

如果这个类型有一个string或者number的类型的索引签名,keyof直接返回这些类型

1
2
3
4
5
6
type Arrayish={[n:number]:unknown};
type A=keypf Arrayish;
//type A=number
type Mapish={[k:string]:boolean};
type M=keyof Mapish;
//type M=string |number

M是stirng|number,这是因为JavaScript对象的属性名会被强制转换为一个字符串,所以obj[0]和obj[“0”]是一样的

数字字面量联合类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const NumericObject ={
[1]:"1";
[2]:"2";
[3]:"3";

}
type result=keyof typeof NumericObject;
// typeof NumbericObject 的结果为:
// {
// 1: string;
// 2: string;
// 3: string;
// }
// 所以最终的结果为:
// type result = 1 | 2 | 3

注意:

下面例子会报错

1
2
3
4
function useKey<T,K extends keyof T>(o:T,k:K){
var name:string=k;
//Type 'string|number|symbol' is not assignable to type 'stirng'
}

如果确定只使用字符串类型的属性名,

1
2
3
function useKey<T,K extends Extract<keyof T,string>>(o:T,k:K){
var name:string=k;//OK
}

如果要处理所有属性名:

1
2
3
function useKey<T,K extends keyof T>(o:T,k:K){
var name:string | number |symbol=k;
}

一个类型需要基于另外一个类型,但是又不想拷贝一份,可以考虑用映射类型

映射修饰符:

在使用映射类型时,两个额外修饰符:readonly用于设置属性只读,?用于设置属性可选,可以通过前缀-或者+删除或者添加这些修饰符,如果没有写前缀,相当于使用了+前缀

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//删除属性中的只读属性
type CreateMutable<Type>={
-readonly [Property in keyof Type]:Type[Property];
}
type LockedAccount={
readonly id:string;
readonly name:string;
}
type UnlockedAccount=CreateMutable<LockedAccount>;
// type UnlockedAccount = {
// id: string;
// name: string;
// }
//删除属性中的可选属性
type Concrete<Type>={
[Property in keyof Type]-?:Type[Property];
}
type MaybeUser={
id:string;
name?:string;
age?:number;
}
type User=Concrete<MaybeUser>;
// type User = {
// id: string;
// name: string;
// age: number;
// }

as实现键名重新映射

1
2
3
type MappedTypeWithProperties<Type>={
[Properties in keyof Type as NewKeyType]:Type[Properties]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
type Getters<Type> = {
[Property in keyof Type as `get${Capitalize<string & Property>}`]: () => Type[Property]
};

interface Person {
name: string;
age: number;
location: string;
}

type LazyPerson = Getters<Person>;

// type LazyPerson = {
// getName: () => string;
// getAge: () => number;
// getLocation: () => string;
// }

参考:

https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

条件类型

基于输入的值的类型决定输出的值的类型,条件类型就是用来帮助我们描述输入类型和输出类型之间的关系

1
2
3
4
5
6
7
8
9
10
11
12
13
interface IdLabel {
id: number /* some fields */;
}
interface NameLabel {
name: string /* other fields */;
}

function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
throw "unimplemented";
}

把逻辑写在条件类型中可以简化掉函数重载:

1
2
3
4
5
6
7
8
9
10
type NameOrId<T extends number|string>=T extends number?IdLabel:NameLabel;
function createLabel<T extends number|string>(idOrName:T):NameOrId<T>{
throw "unimplemented";
}
let a=createLabel("typescript");
//let a:NameLabel
let b=createLabel(2.8);
//let b:IdLabel
let c=createLabel(Math.radom()?"hello":42);
//let c:NameLabel|IdLabel

条件类型约束

1
2
3
4
5
6
7
8
9
10
11
type MessageOf<T extends {message:unknown} ? T["message"]:never;
interface Email {
message:string;
}
interface Dog {
bark():void;
}
type EmailMessageContents=MessageOf<Email>;
//type EmailMessageContents=string
type DogMessageContents=MessageOf<Dog>;
//type DogMessageContents=nerver

在条件类型里推断

infer关键词,可以从正在比较的类型中推断类型,然后在true分支里引用该推断结果

1
2
3
4
5
6
7
type Flatten<T>=T extends any[]?T[number]:T;//number索引用来获取数组元素的类型
type Str=Flatten<string[]>;
//type Str=string
type Num=Flatten<number>;
//type Num=number;
//用infer
type Flatten<Type>=Type extends Array<infer Item>?Item:Type

使用infer写一些有用的类型帮助别名,我们可以获取一个函数返回的类型:

1
2
3
4
5
type GetReturnType<Type>=Type extends (...args:nerver[])=>infer Return ?Return :never;
type Num=GetReturnType<()=>number>;
//type Num=number;
type Str=GetReturnType<(x:string)=>string>;
//type Str=string

当从多重调用签名(比如重载函数)中推断类型时,会按照最后的签名进行推断,因为一般这个签名是用来处理所有情况的签名

分发条件类型

在泛型中使用条件类型时,如果传入一个联合类型,就会变成分发的。

1
2
type ToArray<Type>=Type extends any?Type[]:nerver;

在ToArray传入一个联合类型,这个条件类型会被应用到联合类型的每个成员

1
2
type StrArrOrNumArr=ToArr<string|number>;
//type StrArrOrNumArr=string[]|number[]

先遍历联合类型string|number里的每一个成员,得到ToArray|ToArray,最后结果为string[]|number[]

如果要避免这种行为,用方括号包裹extends关键字的每一部分

1
2
3
4
type ToArrayNonDist<Type>=[Type] extends [any] ? Type[]: nerver;
type StrArrOrNumArr=ToArrayNonDist<string|number>;
//type StrArrOrNumArr = (string | number)[]

先得到ToArrayNonDist<string|number>结果为(string|number)[]

参考:

https://ts.yayujs.com/handbook/ConditionalTypes.html#%E5%88%86%E5%8F%91%E6%9D%A1%E4%BB%B6%E7%B1%BB%E5%9E%8B-distributive-conditional-types

https://www.typescriptlang.org/docs/handbook/2/conditional-types.html

用索引访问类型查找另外一个类型上的特定属性

使用typeof获取数组类型,使用number获取数组元素类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const MyArray=[
{ name: "Alice", age: 15 },
{ name: "Bob", age: 23 },
{ name: "Eve", age: 38 },
];
type Person=typeof MyArray;
//type Person={name:string;age:number}[];
type Person1=typeof MyArray[number];
// type Person = {
// name: string;
// age: number;
// }
type Age=typeof MyArray[number]["age"];
//type Age=number;
type Age2=Person["age"];
//typr Age2=number;

什么情况下可以用T[string]

1
2
3
4
5
interface NumberRecord {
[key: string]: number;
}

type PropType = NumberRecord[string]; // number

这里使用string这个类型访问NumberRecord,由于其内部声明了数字类型的索引签名,这里访问到的结果即是number类型

1
2
3
4
5
6
interface Foo {
propA: number;
propB: boolean;
propC: string;
}
type PropTypeUnion = Foo[keyof Foo]; // string | number | boolean

这里将联合类型每个分支对应的类型进行访问后的结果,重新组成联合类型

在未声明索引签名类型的情况下,我们不能使用NumberRecord[string]这种原始类型的访问方式,而只能通过键名的字面量类型来进行访问

1
2
3
4
5
6
interface Foo {
propA: number;
}

// 类型“Foo”没有匹配的类型“string”的索引签名。
type PropAType = Foo[string];

Array中为什么不能使用Array[string],可以使用Array[length]?

1
2
type Colors = ["white", "red", "black", "purple"]
type ColorsString = Colors[string] //Type 'Colors' has no matching index signature for type 'string'

原因是:

  1. array没有string索引签名
  2. T[length]因为他有一个特定的字符串可以使用
  3. string或number表示所有字符和数字,但这需要索引签名

参考:

https://ts.yayujs.com/handbook/IndexedAccessTypes.html#%E7%B4%A2%E5%BC%95%E8%AE%BF%E9%97%AE%E7%B1%BB%E5%9E%8B-indexed-access-types

https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

typeof

typeof 在类型上下文中使用,用于获取一个变量或者属性的类型,只能对标识符和属性使用

1
2
let s="hello";
let n:typeof s;//let n:string
1
2
let shouldContinue:typeof msgbox("hello");
//',' expected.

要获取msgbox(“hello”)返回值的类型,正确写:

1
ReturnType<typeof msgbox>

ReturnType

你传入一个函数类型,ReturnType会返回该函数的返回值的类型

1
2
type Predicate=(x:unknown)=>boolean;
type K=ReturnType<Predicate>;//type K=boolean

值(values)和类型(types)并不是一种东西,为了获取值f也就是函数f的类型,需要使用typeof,而ReturnType,T必须是一个类型

1
2
3
4
5
6
7
8
9
function f(){
return {x:10,y:3};
}
type P=ReturnType<f>;// 'f' refers to a value, but is being used as a type here. Did you mean 'typeof f'?
type P=ReturnType<typeof f>
// type P = {
// x: number;
// y: number;
// }

对enum类型使用typeof

在TypeScript中,在具体运行时,enum类型会被编译成对象

1
2
3
4
enum UserResponse {
No=0,
Yes=1,
}

对应编译的JavaScript

1
2
3
4
5
var UserResponse;
(function (UserResponse) {
UserResponse[UserResponse["No"] = 0] = "No";
UserResponse[UserResponse["Yes"] = 1] = "Yes";
})(UserResponse || (UserResponse = {}));
1
2
3
4
5
6
7
8
console.log(UserResponse);

// [LOG]: {
// "0": "No",
// "1": "Yes",
// "No": 0,
// "Yes": 1
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
type result = typeof UserResponse;

// ok
const a: result = {
"No": 2,
"Yes": 3
}

result 类型类似于:

// {
// "No": number,
// "YES": number
// }

一般搭配keyof用于获取属性名的联合字符串

1
type result=keyof typeof UserResponse;

参考:

https://ts.yayujs.com/handbook/TypeofTypeOperator.html#%E5%AF%B9-enum-%E4%BD%BF%E7%94%A8-typeof

https://www.typescriptlang.org/docs/handbook/2/typeof-types.html

用来创建可复用组件的工具,我们称之为泛型(generics)。利用泛型,我们可以创建一个支持众多类型的组件,这让用户可以使用自己的类型消费(consume)这些组件

泛型类型变量

1
2
3
4
function identity<Type>(arg:Type):Type{
return arg;
}
let output=identity<string>("myString");//let output:string

Type允许我们捕获用户提供的类型,使得我们接下来可以使用这个类型。调用函数里的<>明确设置Type为string作为函数调用的一个参数

或者直接使用类型参数推断

1
let output=identity("myString")

假如我们要使用arg的.length属性,如果传进的参数不拥有length属性,则会报错,有两种写法:

使用Type类型的数组而不是Type

1
2
3
4
function loggingIdentity<Type>(arg:Type[]):Type[]{
console.log(arg.length);
return arg;
}

泛型函数loggingIdentity接受一个Type类型参数和一个实参arg,实参arg是一个Type类型数组,该函数返回一个Type类型数组,使用类型变量Type,作为我们使用类型的一部分,而不是之前的一整个类型

1
2
3
4
function loggingIdentity<Type>(arg:Array<Type>):Array<Type>{
console.log(arg.length);
return arg;
}

泛型接口

泛型函数作为接口的函数

1
2
3
4
5
6
7
interface GenericIdentityfn{
<Type>(arg:Type):Type;
}
function identity<Type>(arg:Type):Type{
return arg;
}
let myidentity:GenericIdentityfn=identity;

有时,我们会将泛型参数作为整个接口的参数,可以让我们清楚知道传入的是什么参数(GenericIdentityfn而不是GenericIdentityfn),而且接口里的其他成员也可以看到

1
2
3
4
5
6
7
interface GenericIdentityfn{
(arg:Type):Type;
}
function identity<Type>(arg:Type):Type{
return arg;
}
let myidentity:GenericIdentityfn<number>=identity;

泛型类

类似于泛型接口,下面这个例子,不仅可以使用number类型,也可以使用其他类型,和接口一样,可以把类型参数放在类上,确保类中所有属性使用相同类型

一个类有两部分:静态部分和实例部分,泛型类仅仅对实例部分生效,静态成员不能使用类型参数

1
2
3
4
5
6
7
8
9
class GenericNumber<NumType>{
zeroValue:NumType;
add:(x:NumType,y:NumType)=>NumType;
}
let myGenericNumber=new GenericNumber<number>();
myGenericNumber.zeroValue=0;
myGenericNumber.add = function (x, y) {
return x + y;
};

泛型约束

在接口中列出约束条件,然后使用接口和extends关键词实现约束

1
2
3
4
5
6
7
interface Lengthwise {
length:number;
}
function loggingIdentity<Type extends Lengthwise>(arg:Type):Type{
console.log(arg.length);
return arg;
}

使用类型参数约束另一个类型参数

要获取一个对象给定属性名的值,我们需要确保我们捕获获取obj上不存在的属性,所以在两个类型之间建立一个约束:

1
2
3
4
5
6
7
function getProperty<Type,Key extends keyof Type>(obj:Type,key:Key){
return obj[key];
}
let x={a:1,b:2,c:3,d:4};
getProperty(x,'a');
getProperty(x,'m');
// Argument of type '"m"' is not assignable to parameter of type '"a" | "b" | "c" | "d"'.

在泛型中使用类

在TypeScript,当使用工厂模式创建实例时,有必要通过他们的构造函数推断出类的类型

1
2
3
function create<Type>(c:{new():Type}):Type{
return new c();
}

参考:

https://www.typescriptlang.org/docs/handbook/2/generics.html

https://ts.yayujs.com/handbook/Generics.html#%E6%B3%9B%E5%9E%8B-generics

readonly属性

在 TypeScript 中,属性可以被标记为 readonly,这不会改变任何运行时的行为,但在类型检查的时候,一个标记为 readonly的属性是不能被写入的。

不过使用 readonly 并不意味着一个值就完全是不变的,亦或者说,内部的内容是不能变的。readonly 仅仅表明属性本身是不能被重新写入的。

TypeScript 在检查两个类型是否兼容的时候,并不会考虑两个类型里的属性是否是 readonly,这就意味着,readonly 的值是可以通过别名修改的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
interface Person {
name: string;
age: number;
}

interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}

let writablePerson: Person = {
name: "Person McPersonface",
age: 42,
};

// works
let readonlyPerson: ReadonlyPerson = writablePerson;

console.log(readonlyPerson.age); // prints '42'
writablePerson.age++;
console.log(readonlyPerson.age); // prints '43'

索引签名

索引签名用来描述可能的值的类型

1
2
3
4
5
interface StringArray{
[index:number]:string;
}
const myArray:StringArray=getStringArray();
const secondItem=myArray[1];

我们有了一个具有索引签名的接口StringArray,这个索引签名表示当一个StringArray类型的值使用number类型的值进行索引的时候,会返回一个string类型的值

一个索引签名的属性类型必须是string或者是number

虽然TypeScript同时支持string和number类型,但数字索引的返回类型一定要是字符索引返回类型的子类型,因为当使用一个数字进行索引时,JavaScript实际上把它转成了一个字符串,这意味着使用数字100进行索引跟使用字符串100索引是一样的。

1
2
3
4
5
6
7
8
9
10
11
interface Animal {
name:string
}
interface Dog extends Animal {
breed:string;
}
interface NotOkay {
[x:number]:Animal;
[x:string]:Dog;
//error TS2413: 'number' index type 'Animal' is not assignable to 'string' index type 'Dog'.
}

属性继承:

有时我们需要一个比其他类型更具体的类型。举个例子,假设我们有一个 BasicAddress 类型用来描述在美国邮寄信件和包裹的所需字段。

1
2
3
4
5
6
7
8
9
10
11
interface BasicAddress {
name?: string;
street: string;
city: string;
country: string;
postalCode: string;
}

interface AddressWithUnit extends BasicAddress {
unit: string;
}

接口也可以继承多个类型:

1
2
3
4
5
6
7
8
9
10
11
interface Colorful {
color:string;
}
interface Circle {
radius:number;
}
interface ColorfulCircle extends Colorful,Circle{}
const cc:ColorfulCircle={
color:"red",
radius:42,
}

交叉类型

用于合并已经存在的对象类型

1
2
3
4
5
6
7
interface Colorful {
color:string;
}
interface Circle {
radius:number;
}
type ColorfulCircle=Colorful&Circle;

连结Coloeful和Circle产生了一个新的类型,新类型拥有Colorful和Circle的所有成员

1
2
3
4
5
6
7
8
9
10
11
12
function draw(circle:Colorful&Circle){
console.log(`Color was ${circle.color}`);
console.log(`Radius was ${circle.radius}`);
}
// okay
draw({ color: "blue", radius: 42 });

// oops
draw({ color: "red", raidus: 42 });
// Argument of type '{ color: string; raidus: number; }' is not assignable to parameter of type 'Colorful & Circle'.
// Object literal may only specify known properties, but 'raidus' does not exist in type 'Colorful & Circle'. Did you mean to write 'radius'?

接口继承和交叉类型

区别在于冲突怎么处理,这也是选择要使用哪种方式的主要原因

1
2
3
4
5
6
7
8
9
interface Colorful {
color:string;
}
interface ColorfulSub extends Colorful {
color:number;
}
// Interface 'ColorfulSub' incorrectly extends interface 'Colorful'.
// Types of property 'color' are incompatible.
// Type 'number' is not assignable to type 'string'.

使用继承方式,如果重写类型会导致编译错误,但交叉类型不会

1
2
3
4
5
6
interface Colorful {
color:string;
}
type ColorfulSub=Colorful&{
color:number
}

不报错,color属性的类型是nerver,取得的是string和number的交集

类型别名和接口:

类型别名不同于接口,可以描述的不止是对象类型,我们也可以用类型别名写一些其他种类的泛型帮助类型

1
2
3
4
5
6
7
8
9
10
11
type OrNull<Type> = Type | null;

type OneOrMany<Type> = Type | Type[];

type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;

type OneOrManyOrNull<Type> = OneOrMany<Type> | null

type OneOrManyOrNullStrings = OneOrManyOrNull<string>;

type OneOrManyOrNullStrings = OneOrMany<string> | null

元组:

元组类型是另外一种 Array 类型,当你明确知道数组包含多少个元素,并且每个位置元素的类型都明确知道的时候,就适合使用元组类型

1
type StringNumberPair = [string, number];

元组类型在重度依赖约定的 API 中很有用,因为它会让每个元素的意义都很明显。当我们解构的时候,元组给了我们命名变量的自由度。

元组类型中,可以写一个可选属性,但可选元素必须在最后面,而且会影响类型的length

1
2
3
4
5
6
7
8
type Either2dOr3d=[number,number,number?];
function setCoordinate(coord:Either2dOr3d){
const [x,y,z]=coord;
//const z:number|undefined
console.log(`Provided coordinates had ${coord.length} dismensions`);
//(property) length:2|3
}

Tuples可以使用剩余元素语法,但必须是array/tuple类型:

1
2
3
type StringNumberBooleans=[string,number,...boolean[]];
type StringBooleansNumber = [string, ...boolean[], number];
type BooleansStringNumber = [...boolean[], string, number];

有剩余元素的元组不会设置length,因为它只知道在不同位置上的已知元素信息:

1
2
3
4
5
6
7
8
9
const a: StringNumberBooleans = ["hello", 1];
const b: StringNumberBooleans = ["beautiful", 2, true];
const c: StringNumberBooleans = ["world", 3, true, false, true, false, true];

console.log(a.length); // (property) length: number

type StringNumberPair = [string, number];
const d: StringNumberPair = ['1', 1];
console.log(d.length); // (property) length: 2

参数列表使用元组:

1
2
3
function readButtonInput(...args:[string,number,...boolean[]]){
const [name,version,...input]=args;
}

等同于:

1
2
3
function readButtonInput(name:string,version:number,...input:boolean[]){
...
}

readonly元组

大部分代码中,元组只是被创建,使用后不会被修改,所以尽可能将元组设置为readonly是一个好习惯。

1
2
3
4
5
6
7
let point=[3,4] as const;
function distanceFromOrigin([x,y]:[number,number]){
return Math.sqrt(x**2+y**2);
}
distanceFromOrigin(point);
// Argument of type 'readonly [3, 4]' is not assignable to parameter of type '[number, number]'.
// The type 'readonly [3, 4]' is 'readonly' and cannot be assigned to the mutable type '[number, number]'.

尽管distanceFromOrigin并没有更改传入的元素,但函数希望传入一个可变元组,但是point的类型被推断为readonly[3,4],它跟[number,number]并不兼容,所以TypeScript给了一个报错

参考:

https://ts.yayujs.com/handbook/TypeofTypeOperator.html

https://www.typescriptlang.org/docs/handbook/2/typeof-types.html

写一个好的泛型函数建议:

类型参数下移:

1
2
3
4
5
6
7
8
9
10
11
function firstElemet1<Type>(arr:Type[]){
return arr[0];
}
function firstElemet2<Type extends any[]>(arr:Type){
return arr[0];
}
//a:number(good)
const a=firstElemet1([1,2,3])
//b:any(bad)
const b=firstElemet2([1,2,3])

第一个函数可以推断出返回的类型是number,第二个函数推断出的返回类型却是any,因为TypeScript不得不用约束的类型来推断arr[0]表达式,而不是等到函数调用的时候再去推断这个元素。

push down就是如果超类中的某个函数只与一个或者少数几个子类有关,那么最好将其从超类中挪走,放到真正关心它的子类中,即只在超类保留共用的行为,这种将超类中的函数本体复制到具体需要的子类的方法称为”push down”,与本节中的去除extend any[],将其具体的推断交给Type自身就类似于push down.

注意:如果可能,直接使用类型参数而不是约束它

使用更少的类型参数

1
2
3
4
5
6
7
8
9
10
function filter1<Type>(arr: Type[], func: (arg: Type) => boolean): Type[] {
return arr.filter(func);
}

function filter2<Type, Func extends (arg: Type) => boolean>(
arr: Type[],
func: Func
): Type[] {
return arr.filter(func);
}

我们创建了一个并没有关联两个值的类型参数Func,这是一个危险信号,因为它意味着调用者不得不毫无理由的手动指定一个额外的类型参数,Func什么也没做,却导致函数更难阅读和推断

尽可能使用更少的类型参数

类型参数应该出现两次

有的时候一个函数其实并不需要泛型

1
2
3
4
function greet<Str extends string>(s:Str){
console.log("Hello,"+s);
}
greet("world");

类型参数时用来关联多个值之间的类型,如果一个类型参数只在函数签名里出现了一次,那它就没有跟任何东西产生关联

1
2
3
4
function greet(s:string){
console.log("Hello,"+s);

}

在函数中声明this

TypeScript会通过代码流反洗函数中的this会是什么类型

1
2
3
4
5
6
7
const user={
id:124,
admin:false,
becomeAdmin:function(){
this.admin=true;
}
}

TypeScript能够理解函数user.becomeAdmin中的this指向的是外层的对象user,在Js中,this是保留字,不能当做参数使用,但TypeScript可以允许你在函数体内声明this的类型

1
2
3
4
5
6
7
interface DB{
filterUsers(filters:(this:User)=>boolean):User[];
}
const db=getDB();
const admins=db.filterUsers(function(this:User){
return this.admin;
})

function的形式不能使用箭头函数

1
2
3
4
5
6
7
8
interface DB {
filterUsers(filter: (this: User) => boolean): User[];
}

const db = getDB();
const admins = db.filterUsers(() => this.admin);
// The containing arrow function captures the global value of 'this'.
// Element implicitly has an 'any' type because type 'typeof globalThis' has no index signature.

函数的可赋值性:

返回void

当基于上下文的类型推导推导出返回类型为void的时候,并不会强制函数一定不能返回内容,换句话说,如果一个返回void类型的函数类型(type vf=()=>void),当被应用时,也是可以返回任何值的,但返回的值会被忽略,在TS里,子类型的值可以赋值给父类型的变量,在这里,我们可以认为void类型比具体的一些类型,比如number,string类型之类的,更宽泛,因此可以接受这些具体类型。

虽然可以赋值,但是执行函数后返回值的类型,却是void的,并不是实际的真实类型,参考下面的f1变量,它并不是boolean类型,不能访问boolean类型的属性。void类型的用法,主要用在我们不希望调用者关心函数返回值的情况下,比如通常的异步回调函数,这种情况下返回值时没有意义的

1
2
3
4
5
6
7
8
9
10
11
type voidFunc = () => void;

const f1: voidFunc = () => {
return true;
};

const f2: voidFunc = () => true;

const f3: voidFunc = function () {
return true;
};

而且即便这些函数的返回值赋值给其他变量,也会维持void类型

1
2
3
const v1=f1();
const v2=f2();
const v3=f3();

因此,以下代码有效:

1
2
3
const src=[1,2,3];
const dst=[0];
src.forEach((el)=>dst.push(el));

尽管Array.prototype.push返回一个数字,并且Array.prototype.forEach方法期待一个返回void类型的函数,但这段代码依然没有报错,就是因为基于上下文

推导,推导出forEach函数返回类型为void,正是因为不强制函数一定不能返回内容,所有上面这种return dst.push(el)的写法不报错

当一个函数字面量定义返回一个void类型,函数一定不能返回任何东西

1
2
3
4
5
6
7
8
9
function f2(): void {
// @ts-expect-error
return true;
}

const f3 = function (): void {
// @ts-expect-error
return true;
};

具体参考:

https://ts.yayujs.com/handbook/MoreOnFunctions.html#%E5%87%BD%E6%95%B0-more-on-functions

https://www.typescriptlang.org/docs/handbook/2/functions.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.bind=function(context){
if(typeof this !=='function'){
throw new Error("Function .prototype.bind-what is trying to be bound is not callable")
}
var self=this;
var args=Array.prototype.slice.call(arguments,1);
var fNOP=function(){};
var fBound=function(){
var bindArgs=Array.prototype.slice.call(arguments);
return self.apply(this instanceof fNOP?this:context,args.concat(bindArgs));
}
fNOP.prototype=this.prototype;
fBound.prototype=new fNOP();
return fBound;

}

参考:

[]: https://github.com/mqyqingfeng/Blog/issues/12