当前位置:   article > 正文

TypeScript学习(类型推断、类型的兼容性)_typescript 推断构造函数参数

typescript 推断构造函数参数

类型推断

定义变量

变量的类型,由定义推断

let foo = 123; // foo 是 'number'
let bar = 'hello'; // bar 是 'string'

foo = bar; // Error: 不能将 'string' 赋值给 `number`
  • 1
  • 2
  • 3
  • 4

函数返回类型

function add(a: number, b: number) {
  return a + b;
}
  • 1
  • 2
  • 3

赋值

函数参数类型/返回值也能通过赋值来推断。如下所示,foo 的类型是 Adder,他能让 foo 的参数 a、b 是 number 类型。

type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => a + b;
  • 1
  • 2

TypeScript 会发出正如你期望发出的错误警告:

type Adder = (a: number, b: number) => number;
let foo: Adder = (a, b) => {
  a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  return a + b;
};
  • 1
  • 2
  • 3
  • 4
  • 5

如果你创建一个函数,并且函数参数为一个回调函数,相同的赋值规则也适用于它。从 argument 至 parameter 只是变量赋值的另一种形式。

type Adder = (a: number, b: number) => number;
function iTakeAnAdder(adder: Adder) {
  return adder(1, 2);
}

iTakeAnAdder((a, b) => {
  a = 'hello'; // Error: 不能把 'string' 类型赋值给 'number' 类型
  return a + b;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

结构化

这些简单的规则也适用于结构化的存在(对象字面量),例如在下面这种情况下 foo 的类型被推断为 { a: number, b: number }:

const foo = {
  a: 123,
  b: 456
};

foo.a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型

const bar = [1, 2, 3];
bar[0] = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

解构

const foo = {
  a: 123,
  b: 456
};
let { a } = foo;

a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
const bar = [1, 2];
let [a, b] = bar;

a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  • 1
  • 2
  • 3
  • 4

如果函数参数能够被推断出来,那么解构亦是如此。在如下例子中,函数参数能够被解构为 a/b 成员:

type Adder = (number: { a: number; b: number }) => number;
function iTakeAnAdder(adder: Adder) {
  return adder({ a: 1, b: 2 });
}

iTakeAnAdder(({ a, b }) => {
  // a, b 的类型能被推断出来
  a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  return a + b;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
type Adder = (number: { a: number; b: number }) => number;
function iTakeAnAdder(adder: Adder) {
  return adder({ a: 1, b: 2 });
}

iTakeAnAdder(({ a, b }) => {
  // a, b 的类型能被推断出来
  a = 'hello'; // Error:不能把 'string' 类型赋值给 'number' 类型
  return a + b;
});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

类型保护

小心使用参数

如果类型不能被赋值推断出来,类型也将不会流入函数参数中。例如如下的一个例子,编译器并不知道 foo 的类型,所它也就不能推断出 a 或者 b 的类型。

const foo = (a, b) => {
  /* do something */
};
  • 1
  • 2
  • 3

然而,如果 foo 添加了类型注解,函数参数也就能被推断(a,b 都能被推断为 number 类型):

type TwoNumberFunction = (a: number, b: number) => void;
const foo: TwoNumberFunction = (a, b) => {
  /* do something */
};
  • 1
  • 2
  • 3
  • 4

小心使用返回值

尽管 TypeScript 一般情况下能推断函数的返回值,但是它可能并不是你想要的。例如如下的 foo 函数,它的返回值为 any:

function foo(a: number, b: number) {
  return a + addOne(b);
}

// 一些使用 JavaScript 库的特殊函数
function addOne(a) {
  return a + 1;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

这是因为返回值的类型被一个缺少类型定义的 addOne 函数所影响(a 是 any,所以 addOne 返回值为 any,foo 的返回值是也是 any)。

TIP
最好给所有函数明确的写上函数的返回值

这里还有一些其他可以想象的情景,但是有一个好消息是有编译器选项 noImplicitAny 可以捕获这些 bug。

noImplicitAny

选项 noImplicitAny 用来告诉编译器,当无法推断一个变量时发出一个错误(或者只能推断为一个隐式的 any 类型),你可以:

通过显式添加 :any 的类型注解,来让它成为一个 any 类型;
通过一些更正确的类型注解来帮助 TypeScript 推断类型。

类型的兼容性

类型兼容性用于确定一个类型是否能赋值给其他类型。

let str: string = 'Hello';
let num: number = 123;

str = num; // Error: 'number' 不能赋值给 'string'
num = str; // Error: 'string' 不能赋值给 'number'
  • 1
  • 2
  • 3
  • 4
  • 5

安全性

TypeScript 类型系统设计比较方便,它允许你有一些不正确的行为。例如:任何类型都能被赋值给 any,这意味着告诉编译器你可以做任何你想做的事情:

let foo: any = 123;
foo = 'hello';

foo.toPrecision(3);
  • 1
  • 2
  • 3
  • 4

结构化

TypeScript 对象是一种结构类型,这意味着只要结构匹配,名称也就无关紧要了:

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

class Point2D {
  constructor(public x: number, public y: number) {}
}

let p: Point;

// ok, 因为是结构化的类型
p = new Point2D(1, 2);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

这允许你动态创建对象(就好像你在 vanilla JS 中使用一样),并且它如果能被推断,该对象仍然具有安全性。可以多给值但是不能少给

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

interface Point3D {
  x: number;
  y: number;
  z: number;
}

const point2D: Point2D = { x: 0, y: 10 };
const point3D: Point3D = { x: 0, y: 10, z: 20 };
function iTakePoint2D(point: Point2D) {
  /* do something */
}

iTakePoint2D(point2D); // ok, 完全匹配
iTakePoint2D(point3D); // 额外的信息,没关系
iTakePoint2D({ x: 0 }); // Error: 没有 'y'

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

变体

对一个简单类型 Base 和 Child 来说,如果 Child 是 Base 的子类,Child 的实例能被赋值给 Base 类型的变量。

在由 Base 和 Child 组合的复杂类型的类型兼容性中,它取决于相同场景下的 Base 与 Child 的变体:

  • 协变(Covariant):只在同一个方向;
  • 逆变(Contravariant):只在相反的方向;
  • 双向协变(Bivariant):包括同一个方向和不同方向;
  • 不变(Invariant):如果类型不完全相同,则它们是不兼容的。

函数

返回类型

协变(Covariant):返回类型必须包含足够的数据。

interface Point2D {
  x: number;
  y: number;
}
interface Point3D {
  x: number;
  y: number;
  z: number;
}

let iMakePoint2D = (): Point2D => ({ x: 0, y: 0 });
let iMakePoint3D = (): Point3D => ({ x: 0, y: 0, z: 0 });

iMakePoint2D = iMakePoint3D;
iMakePoint3D = iMakePoint2D; // ERROR: Point2D 不能赋值给 Point3D
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

参数数量

更少的参数数量是好的(如:函数能够选择性的忽略一些多余的参数),但是你得保证有足够的参数被使用了:


const iTakeSomethingAndPassItAnErr = (x: (err: Error, data: any) => void) => {
  /* 做一些其他的 */
};

iTakeSomethingAndPassItAnErr(() => null); // ok
iTakeSomethingAndPassItAnErr(err => null); // ok
iTakeSomethingAndPassItAnErr((err, data) => null); // ok

// Error: 参数类型 `(err: any, data: any, more: any) => null` 不能赋值给参数类型 `(err: Error, data: any) => void`
iTakeSomethingAndPassItAnErr((err, data, more) => null);
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

可选的和 rest 参数

可选的(预先确定的)和 Rest 参数(任何数量的参数)都是兼容的:

let foo = (x: number, y: number) => {};
let bar = (x?: number, y?: number) => {};
let bas = (...args: number[]) => {};

foo = bar = bas;
bas = bar = foo;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

函数参数类型

双向协变(Bivariant):旨在支持常见的事件处理方案。

// 事件等级
interface Event {
  timestamp: number;
}
interface MouseEvent extends Event {
  x: number;
  y: number;
}
interface KeyEvent extends Event {
  keyCode: number;
}

// 简单的事件监听
enum EventType {
  Mouse,
  Keyboard
}
function addEventListener(eventType: EventType, handler: (n: Event) => void) {
  // ...
}

// 不安全,但是有用,常见。函数参数的比较是双向协变。
addEventListener(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));

// 在安全情景下的一种不好方案
addEventListener(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));
addEventListener(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));

// 仍然不允许明确的错误,对完全不兼容的类型会强制检查
addEventListener(EventType.Mouse, (e: number) => console.log(e));
  • 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
  • 29
  • 30

枚举

枚举与数字类型相互兼容

enum Status {
  Ready,
  Waiting
}

let status = Status.Ready;
let num = 0;

status = num;
num = status;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

来自于不同枚举的枚举变量,被认为是不兼容的

enum Status {
  Ready,
  Waiting
}
enum Color {
  Red,
  Blue,
  Green
}

let status = Status.Ready;
let color = Color.Red;

status = color; // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

仅仅只有实例成员和方法会相比较,构造函数和静态成员不会被检查。

class Animal {
  feet: number;
  constructor(name: string, numFeet: number) {}
}

class Size {
  feet: number;
  constructor(meters: number) {}
}

let a: Animal;
let s: Size;

a = s; // OK
s = a; // OK
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

私有的和受保护的成员必须来自于相同的类。

class Animal {
  protected feet: number;
}
class Cat extends Animal {}

let animal: Animal;
let cat: Cat;

animal = cat; // ok
cat = animal; // ok

class Size {
  protected feet: number;
}

let size: Size;

animal = size; // ERROR
size = animal; // ERROR
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

泛型

TypeScript 类型系统基于变量的结构,仅当类型参数在被一个成员使用时,才会影响兼容性。如下例子中,T 对兼容性没有影响:

interface Empty<T> {}

let x: Empty<number>;
let y: Empty<string>;

x = y; // ok
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

当 T 被成员使用时,它将在实例化泛型后影响兼容性:

interface Empty<T> {
  data: T;
}

let x: Empty<number>;
let y: Empty<string>;

x = y; // Error
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

如果尚未实例化泛型参数,则在检查兼容性之前将其替换为 any:


let identity = function<T>(x: T): T {
  // ...
};

let reverse = function<U>(y: U): U {
  // ...
};

identity = reverse; // ok, 因为 `(x: any) => any` 匹配 `(y: any) => any`
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

类中的泛型兼容性与前文所提及一致


class List<T> {
  add(val: T) {}
}

class Animal {
  name: string;
}
class Cat extends Animal {
  meow() {
    // ..
  }
}

const animals = new List<Animal>();
animals.add(new Animal()); // ok
animals.add(new Cat()); // ok

const cats = new List<Cat>();
cats.add(new Animal()); // Error
cats.add(new Cat()); // ok
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

脚注:不变性(Invariance)

我们说过,不变性可能是唯一一个听起来合理的选项,这里有一个关于 contra 和 co 的变体,被认为对数组是不安全的。

class Animal {
  constructor(public name: string) {}
}
class Cat extends Animal {
  meow() {
    console.log('cat');
  }
}

let animal = new Animal('animal');
let cat = new Cat('cat');

// 多态
// Animal <= Cat

animal = cat; // ok
cat = animal; // ERROR: cat 继承于 animal

// 演示每个数组形式
let animalArr: Animal[] = [animal];
let catArr: Cat[] = [cat];

// 明显的坏处,逆变
// Animal <= Cat
// Animal[] >= Cat[]
catArr = animalArr; // ok, 如有有逆变
catArr[0].meow(); // 允许,但是会在运行时报错

// 另外一个坏处,协变
// Animal <= Cat
// Animal[] <= Cat[]
animalArr = catArr; // ok,协变

animalArr.push(new Animal('another animal')); // 仅仅是 push 一个 animal 至 carArr 里
catArr.forEach(c => c.meow()); // 允许,但是会在运行时报错。
  • 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
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家小花儿/article/detail/113239
推荐阅读
相关标签
  

闽ICP备14008679号