TypeScript学习(类型保护/字面量类型/readonly)

类型保护

类型保护允许你使用更小范围下的对象类型

typeof

function doSome(x: number | string) {
    
    
  if (typeof x === 'string') {
    
    
    // 在这个块中,TypeScript 知道 `x` 的类型必须是 `string`
    console.log(x.subtr(1)); // Error: 'subtr' 方法并没有存在于 `string` 上
    console.log(x.substr(1)); // ok
  }

  x.substr(1); // Error: 无法保证 `x` 是 `string` 类型
}

instanceof

这有一个关于 class 和 instanceof 的例子:

class Foo {
    
    
  foo = 123;
  common = '123';
}

class Bar {
    
    
  bar = 123;
  common = '123';
}

function doStuff(arg: Foo | Bar) {
    
    
  if (arg instanceof Foo) {
    
    
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  }
  if (arg instanceof Bar) {
    
    
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

doStuff(new Foo());
doStuff(new Bar());

TypeScript 甚至能够理解 else。当你使用 if 来缩小类型时,TypeScript 知道在其他块中的类型并不是 if 中的类型:

class Foo {
    
    
  foo = 123;
}

class Bar {
    
    
  bar = 123;
}

function doStuff(arg: Foo | Bar) {
    
    
  if (arg instanceof Foo) {
    
    
    console.log(arg.foo); // ok
    console.log(arg.bar); // Error
  } else {
    
    
    // 这个块中,一定是 'Bar'
    console.log(arg.foo); // Error
    console.log(arg.bar); // ok
  }
}

doStuff(new Foo());
doStuff(new Bar());

in(ts)

in 操作符可以安全的检查一个对象上是否存在一个属性,它通常也被作为类型保护使用:

interface A {
    
    
  x: number;
}

interface B {
    
    
  y: string;
}

function doStuff(q: A | B) {
    
    
  if ('x' in q) {
    
    
    // q: A
  } else {
    
    
    // q: B
  }
}

字面量类型保护

字面量是 JavaScript 本身提供的一个准确变量。

字符串字面量

你可以使用一个字符串字面量作为一个类型:

let foo: 'Hello';

在这里,我们创建了一个被称为 foo 变量,它仅接收一个字面量值为 Hello 的变量:

let foo: 'Hello';
foo = 'Bar'; // Error: 'bar' 不能赋值给类型 'Hello'

它们本身并不是很实用,但是可以在一个联合类型中组合创建一个强大的(实用的)抽象:

type CardinalDirection = 'North' | 'East' | 'South' | 'West';
function move(distance: number, direction: CardinalDirection) {
    
    
  // ...
}

move(1, 'North'); // ok
move(1, 'Nurth'); // Error

其他字面量类型

TypeScript 同样也提供 boolean 和 number 的字面量类型:

type OneToFive = 1 | 2 | 3 | 4 | 5;
type Bools = true | false;

推断

通常,你会得到一个类似于 Type string is not assignable to type ‘foo’ 的错误,如下:

function iTakeFoo(foo: 'foo') {
    
    }
const test = {
    
    
  someProp: 'foo'
};

iTakeFoo(test.someProp); // Error: Argument of type string is not assignable to parameter of type 'foo'

这是由于 test 被推断为 { someProp: string },我们可以采用一个简单的类型断言来告诉 TypeScript 你想推断的字面量:

function iTakeFoo(foo: 'foo') {
    
    }

const test = {
    
    
  someProp: 'foo' as 'foo'
};

iTakeFoo(test.someProp); // ok

或者使用类型注解的方式,来帮助 TypeScript 推断正确的类型:
一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。

function iTakeFoo(foo: 'foo') {
    
    }

type Test = {
    
    
  someProp: 'foo';
};

const test: Test = {
    
    
  // 推断 `someProp` 永远是 'foo'
  someProp: 'foo'
};

iTakeFoo(test.someProp); // ok

使用用例

TypeScript 枚举类型是基于数字的,你可以使用带字符串字面量的联合类型,来模拟一个基于字符串的枚举类型,就好像上文中提出的 CardinalDirection。你甚至可以使用下面的函数来生成 key: value 的结构:

// 用于创建字符串列表映射至 `K: V` 的函数
function strEnum<T extends string>(o: Array<T>): {
    
     [K in T]: K } {
    
    
  return o.reduce((res, key) => {
    
    
    res[key] = key;
    return res;
  }, Object.create(null));
}

然后,你就可以使用 keyof、typeof 来生成字符串的联合类型。下面是一个完全的例子:

  • keyof 见名知其意,就是获取对象所有的key,然后返回一个新的联合类型。
  • 见keyof和typeof详解
// 用于创建字符串列表映射至 `K: V` 的函数
function strEnum<T extends string>(o: Array<T>): {
    
     [K in T]: K } {
    
    
  return o.reduce((res, key) => {
    
    
    res[key] = key;
    return res;
  }, Object.create(null));
}

// 创建 K: V
const Direction = strEnum(['North', 'South', 'East', 'West']);

// 创建一个类型
type Direction = keyof typeof Direction;

// 简单的使用
let sample: Direction;

sample = Direction.North; // Okay
sample = 'North'; // Okay
sample = 'AnythingElse'; // ERROR!

readonly(只读)

TypeScript 类型系统允许你在一个接口里使用 readonly 来标记属性。它能让你以一种更安全的方式工作(不可预期的改变是很糟糕的):

function foo(config: {
    
     readonly bar: number, readonly bas: number }) {
    
    
  // ..
}

const config = {
    
     bar: 123, bas: 123 };
foo(config);

// 现在你能够确保 'config' 不能够被改变了

在 interface 和 type 里使用 readonly:

type Foo = {
    
    
  readonly bar: number;
  readonly bas: number;
};

// 初始化
const foo: Foo = {
    
     bar: 123, bas: 456 };

// 不能被改变
foo.bar = 456; // Error: foo.bar 为仅读属性

指定一个类的属性为只读,然后在声明时或者构造函数中初始化它们,如下所示:

class Foo {
    
    
  readonly bar = 1; // OK
  readonly baz: string;
  constructor() {
    
    
    this.baz = 'hello'; // OK
  }
}

Readonly

这有一个 Readonly 的映射类型,它接收一个泛型 T,用来把它的所有属性标记为只读类型:

type Foo = {
    
    
  bar: number;
  bas: number;
};

type FooReadonly = Readonly<Foo>;

const foo: Foo = {
    
     bar: 123, bas: 456 };
const fooReadonly: FooReadonly = {
    
     bar: 123, bas: 456 };

foo.bar = 456; // ok
fooReadonly.bar = 456; // Error: bar 属性只读

其他的使用用例

  • ReactJS
    ReactJS 是一个喜欢用不变数据的库,你可以标记你的 Props 和 State 为不可变数据
interface Props {
    
    
  readonly foo: number;
}

interface State {
    
    
  readonly bar: number;
}

export class Something extends React.Component<Props, State> {
    
    
  someMethod() {
    
    
    // 你可以放心,没有人会像下面这么做
    this.props.foo = 123; // Error: props 是不可变的
    this.state.baz = 456; // Error: 你应该使用 this.setState()
  }
}

然而,你并没有必要这么做,React 的声明文件已经标记这些为 readonly(通过传入泛型参数至一个内部包装,来把每个属性标记为 readonly,如上例子所示),

export class Something extends React.Component<{
    
     foo: number }, {
    
     baz: number }> {
    
    
  someMethod() {
    
    
    this.props.foo = 123; // Error: props 是不可变的
    this.state.baz = 456; // Error: 你应该使用 this.setState()
  }
}

绝对的不可变

你甚至可以把索引签名标记为只读:

interface Foo {
    
    
  readonly [x: number]: number;
}

// 使用

const foo: Foo = {
    
     0: 123, 2: 345 };
console.log(foo[0]); // ok(读取)
foo[0] = 456; // Error: 属性只读

如果你想以不变的方式使用原生 JavaScript 数组,可以使用 TypeScript 提供的 ReadonlyArray 接口:

let foo: ReadonlyArray<number> = [1, 2, 3];
console.log(foo[0]); // ok
foo.push(4); // Error: ReadonlyArray 上不存在 `push`,因为他会改变数组
foo = foo.concat(4); // ok, 创建了一个复制

自动推断

在一些情况下,编译器能把一些特定的属性推断为 readonly,例如在一个 class 中,如果你有一个只含有 getter 但是没有 setter 的属性,他能被推断为只读:

class Person {
    
    
  firstName: string = 'John';
  lastName: string = 'Doe';

  get fullName() {
    
    
    return this.firstName + this.lastName;
  }
}

const person = new Person();

console.log(person.fullName); // John Doe
person.fullName = 'Dear Reader'; // Error, fullName 只读

与 const 的不同

  • const

    用于变量;
    变量不能重新赋值给其他任何事物。

  • readonly
    用于属性;
    用于别名,可以修改属性;

简单的例子 1:

const foo = 123; // 变量
let bar: {
    
    
  readonly bar: number; // 属性
};

简单的例子 2:

const foo: {
    
    
  readonly bar: number;
} = {
    
    
  bar: 123
};

function iMutateFoo(foo: {
    
     bar: number }) {
    
    
  foo.bar = 456;
}

iMutateFoo(foo);
console.log(foo.bar); // 456

readonly能确保“我”不能修改属性,但是当你把这个属性交给其他并没有这种保证的使用者(允许出于类型兼容性的原因),他们能改变它。当然,如果 iMutateFoo 明确的表示,他们的参数不可修改,那么编译器会发出错误警告:

interface Foo {
    
    
  readonly bar: number;
}

let foo: Foo = {
    
    
  bar: 123
};

function iTakeFoo(foo: Foo) {
    
    
  foo.bar = 456; // Error: bar 属性只读
}

iTakeFoo(foo);

おすすめ

転載: blog.csdn.net/shadowfall/article/details/121085627