1

1. 赋值断言

  • 类属性使用赋值断言
    官方示例:

    class C {
    foo!: number;
    // ^
    // Notice this exclamation point!
    // This is the "definite assignment assertion" modifier.
    constructor() {
      this.initialize();
    }
    
    initialize() {
      this.foo = 0;
    }
    }

    如果去掉foo后面的感叹号会怎样?

果断报错

报错提示我们foo没有初始化,也没有在构造函数中定义;意思就是在构造函数中初始化就不会报错了?

在构造函数中初始化

在构造函数中初始化的确不报错,反向证明报错信息很准确。

  • 变量声明使用赋值断言

官方示例

let x!: number[];
initialize();
x.push(4);

function initialize() {
  x = [0, 1, 2, 3];
}

去掉感叹号

去掉感叹号

报错信息告诉我们变量x在被赋值之前使用了。

提示:

上面的报错信息都是建立在tsconfig.json设置strict:true的前提下,
在不使用赋值断言的前提下:

strictPropertyInitializationstrict变量类属性
falsetrue报错不报错
falsefalse不报错不报错
truefalse不报错报错
truetrue报错报错

strictPropertyInitialization的粒度更细,变量没有初始化导致报错没有更细粒度的参数去配置。

小结:对于延迟初始化,typescript没法推断出来,要通过赋值断言显示指定后面会为其初始化。

2. readonly vs as const

  • as const 的作用是将被修饰的所有属性都设置为只读状态(注意修饰的是值,而不是变量)
  • readonly 用于修饰interface type class声明的属性

type

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

class

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

对于只读数组可以用ReadonlyArray<T>

let foo: ReadonlyArray<number> = [1, 2, 3];

对于函数式编程的要求之一就是函数是纯函数,其中一个对象或者数组传递到纯函数中,该参数不应该被改变。有没有类型去修饰函数参数是不可变的?
Can a typescript parameter be annotated as const?做了说明,还没有类型能直接修饰参数是不可变的,但是用readonly可以每一层去指定该参数是不可变的。比如

function foo(arr: readonly string[]) {}

经过这么修饰arr就不能再调用push方法。一般后端返回的数据不会有太多层,我们可以在数据的每一层去用readonly修饰,比如:

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

function foo(config: Foo) {
  config.children.biu = 99;
}

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

这样赋值的时候就会报错:
报错信息

readonly做了详细的解释

as const 也不能修饰变量,只能修饰确定的值而不是变量,比如

image.png

3. 覆盖、扩展node_modules下的类型定义

我想在覆盖umi@3.x的useSelector达到使用的时候不传递state类型的目的,在一个d.ts中定义:

import 'react-redux'

export interface GlobalState {
  loading: Loading
  user: UserState
}

declare module 'react-redux' {
  export function useSelector<TState = GlobalState, TSelected = unknown>(
    selector: (state: TState) => TSelected,
    equalityFn?: (left: TSelected, right: TSelected) => boolean
  ): TSelected
}

这样我们在页面中就可以直接使用,不传递任何类型,而获得良好的代码提示

  const age = useSelector((state) => state.user.age)

使用umi@4.0.90后上面的方式没法用了,换个方式,
根目录下的 typings.d.ts

import { FileManagerModelState } from '@/pages/file-manager/model';
declare module 'lodash';

function useSelector<TState = RootState, TSelected = unknown>(
  selector: (state: TState) => TSelected,
  equalityFn?: (left: TSelected, right: TSelected) => boolean,
): TSelected;

export interface RootState {
  // loading: Loading;
  fileManager: FileManagerModelState;
}

declare global {
  type Nullable<T> = T | null;
}

declare module 'umi' {
  export { useSelector }; // 这里很关键
}

参考自:Force Override Declarations Types #36146

Typescript声明文件-第三方类型扩展

TypeScript模块扩展变成覆盖原模块的解决方案

TypeScript 声明文件全解析

4. 从一个文件导入类型 再将其导出 不加type关键字 会有warning

utils.ts

import { ColumnGroupType, ColumnType } from 'antd/es/table/index'

export type IColumnsType<RecordType = unknown> = Array<
  | ColumnGroupType<RecordType>
  | ColumnType<RecordType>
  | { maxWidth?: number; minWidth: number }
>

export interface X {
  title: string
}

index.tsx

import {
  IColumnsType,
  X
} from './utils'

interface IProps
  extends Omit<TableProps<any>, 'onChange' | 'columns' | 'dataSource'> {
  columns: IColumnsType
}

export type { IColumnsType, X }

如果直接 export { IColumnsType } 不加上 type 关键字 会有warning ,对于interface同样适用。

warning

TypeScript export imported interface 提到 when '--isolatedModules' flag is provided it requires using 'export type'. 我的tsconfig.json并没有配置isolatedModules,我的ts@4.3.2

5. 'string' is assignable to the constraint of type 'I', but 'I' could be instantiated with a different subtype of constraint 'string | number'

查看 TypeScript 类型编程: 从基础到编译器实战 基于泛型创建新类型 - 函数调用,对函数泛型的写法感到新奇,写了个例子:

type Fn2<T> = <I extends string | number>(a: I) => T;

const f2: Fn2<string> = (a) => {
  a = "scscd";
  return "xxx";
};

image.png

上述的例子并不好理解,这篇问答的例子好理解点,How to fix TS2322: "could be instantiated with a different subtype of constraint 'object'"?有详细的解释。

用到的类型 数据

type Foo         =  { readonly 0: '0'}
type SubType     =  { readonly 0: '0', readonly a: 'a'}
type DiffSubType =  { readonly 0: '0', readonly b: 'b'}

const foo:             Foo         = { 0: '0'}
const foo_SubType:     SubType     = { 0: '0', a: 'a' }
const foo_DiffSubType: DiffSubType = { 0: '0', b: 'b' }

定义泛型的函数:

const func = <A extends Foo>(a: A = foo_SubType) => `hello!`; //error!

image.png
出现跟上述一样的错误,翻译过来就是SubType可以指给类型A,但是A可能会被Foo的不同子类实例化。所以foo_SubType不能赋值给A。extends限定了A是Foo类型或者其子类型,但是在函数定义的时候A并不能确定是哪个子类,所以函数定义的时候在函数体内无法给a赋值具体的值。如果我想在函数体内对a进行赋值咋办?上面的函数定义方式没法对a在函数体内进行赋值。好,我们变换种形式,

type Fn3 = <A extends Foo>(a: A) => string;

const func3: Fn3 = (a: Foo = foo_SubType) => {
  a = foo_SubType;
  return `hello!`;
};

将函数的类型提取出来,然后再定义类型为Fn3的func3函数,然后给a指定Foo类型,这样就可以给a赋默认值或者在函数体内对a赋值。这么操作相当于把泛型变成了固定类型。如果不给a指定Foo类型,直接赋值,依然会报一样的错误。最开始的f2跟func3一个道理,如果想在函数体内给a赋值,则需要把泛型固定下来。换句话说,不固定泛型类型的函数参数是没法直接赋值的。

type Fn2<T> = <I extends string>(a: I) => T;

const f2: Fn2<string> = (a: string | boolean) => {
  a = "xxx";
  return "xxx";
};

上述两种函数参数泛型的写法在调用的时候没有任何区别,只是在函数体内对泛型参数的赋值有区别。

func<Foo>(foo);
func<SubType>(foo_SubType);
func<DiffSubType>(foo_DiffSubType);

func(foo);
func(foo_SubType);
func(foo_DiffSubType);

func3<Foo>(foo);
func3<SubType>(foo_SubType);
func3<DiffSubType>(foo_DiffSubType);

func3(foo);
func3(foo_SubType);
func3(foo_DiffSubType);

总结:
泛型函数参数类型不固定,所以在函数体内无法直接赋值。

6. 给指定的类型赋空对象,报错

image.png

报错信息

image.png

解决办法
currentDialog: <IDialog>{}, 或者 currentDialog: {} as IDialog,

TypeScript empty object for a typed variable


assassin_cike
1.3k 声望74 粉丝

生活不是得过且过


« 上一篇
redux-saga初探