3

typescript 实用小技巧

 3 years ago
source link: https://blog.dteam.top/posts/2021-08/ts-useful-tips.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

typescript 实用小技巧

小纪同学 Posted at — Aug 24, 2021 阅读 3

用了很久的 typescript,用了但感觉又没完全用。因为很多 typescript 的特性没有被使用,查看之前写的代码满屏的 any,这样就容易导致很多 bug,也没有发挥出 typescript 真正的“类型”威力。本文总结了一些使用 typescript 的小技巧,以后使用 typescript 时可以运用起来。

废话不多说,直接上代码。

当希望传 user 参数时,不传 flag,传 para 时,传 flag。就可以这样写:

interface User {
  name: string;
  age: number;
}

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {

  public test(para: User): number;
  public test(para: number, flag: boolean): number;

  public test(para: User | number, flag?: boolean): number {
    // 具体实现
    return 1;
  }
}

const someClass = new SomeClass();

// ok
someClass.test(user);
someClass.test(123, false);

// Error
// someClass.test(123); 
//Argument of type 'number' is not assignable to parameter of type 'User'.
// someClass.test(user, false);
//Argument of type '{ name: string; age: number; }' is not assignable to parameter of type 'number'.

在了解映射类型之前,需要了解 keyof, never, typeof, in。

  • keyof:keyof 取 interface 的键
interface Point {
    x: number;
    y: number;
}

// type keys = "x" | "y"
type keys = keyof Point;
  • never:永远不存在的值的类型

官方描述:

the never type represents the type of values that never occur.

// 例子:进行编译时的全面的检查
type Foo = string | number;

function controlFlowAnalysisWithNever(foo: Foo) {
  if (typeof foo === "string") {
    // 这里 foo 被收窄为 string 类型
  } else if (typeof foo === "number") {
    // 这里 foo 被收窄为 number 类型
  } else {
    // foo 在这里是 never
    const check: never = foo;
  }
}

使用 never 避免出现新增了联合类型没有对应的实现,目的就是写出类型绝对安全的代码。

  • typeof:取某个值的 type
const a: number = 3

// 相当于: const b: number = 4
const b: typeof a = 4
  • in:检查一个对象上是否存在一个属性
interface A {
  x: number;
}

interface B {
  y: string;
}

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

映射类型就是将一个类型映射成另外一个类型,简单理解就是新类型以相同的形式去转换旧类型的每个属性。

Partial, Readonly, Nullable, Required

  • Partial 将每个属性转换为可选属性
  • Readonly 将每个属性转换为只读属性
  • Nullable 转换为旧类型和null的联合类型
  • Required 将每个属性转换为必选属性
type Partial<T> = {
    [P in keyof T]?: T[P];
}

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
}

type Nullable<T> = { 
  [P in keyof T]: T[P] | null 
}

type Required<T> = {
  [P in keyof T]-?: T[P]
}

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

type PersonPartial = Partial<Person>;
type PersonReadonly = Readonly<Person>;
type PersonNullable = Nullable<Person>;

type PersonPartial = {
    name?: string | undefined;
    age?: number | undefined;
}

type PersonReadonly = {
    readonly name: string;
    readonly age: number;
}

type PersonNullable = {
      name: string | null;
      age: number | null;
}

interface Props {
  a?: number;
  b?: string;
}

const obj: Props = { a: 5 };

const obj2: Required<Props> = { a: 5 };
// Property 'b' is missing in type '{ a: number; }' but required in type 'Required<Props>'.

Pick, Record

  • Pick 选取一组属性指定新类型
  • Record 创建一组属性指定新类型,常用来声明普通Object对象
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
}

type Record<K extends keyof any, T> = {
  [P in K]: T;
}

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Pick<Todo, "title" | "completed">;

const todo: TodoPreview = {
  title: "Clean room",
  completed: false,
};

todo; // = const todo: TodoPreview


interface PageInfo {
  title: string;
}

type Page = "home" | "about" | "contact";

const nav: Record<Page, PageInfo> = {
  about: { title: "title1" },
  contact: { title: "title2" },
  home: { title: "title3" },
};

nav.about; // = const nav: Record

Exclude, Omit

  • Exclude 去除交集,返回剩余的部分
  • Omit 适用于键值对对象的Exclude,去除类型中包含的键值对
type Exclude<T, U> = T extends U ? never : T
type Omit = Pick<T, Exclude<keyof T, K>>

// 相当于: type A = 'a'
type A = Exclude<'x' | 'a', 'x' | 'y' | 'z'>

interface Todo {
  title: string;
  description: string;
  completed: boolean;
}

type TodoPreview = Omit<Todo, "description">;

const todo: TodoPreview = {
  title: "a",
  completed: false,
};

ReturnType

获取返回值类型,一般为函数

type ReturnType<T extends (...args: any) => any>
  = T extends (...args: any) => infer R ? R : any;

declare function f1(): { a: number; b: string };
type T1 = ReturnType<typeof f1>;
//    type T1 = {
//        a: number;
//        b: string;
//    }

还有很多映射类型,可查看Utility Types参考。

类型断言用来明确的告诉 typescript 值的详细类型,合理使用能减少我们的工作量。

比如一个变量并没有初始值,但是我们知道它的类型信息(它可能是从后端返回)有什么办法既能正确推导类型信息,又能正常运行了?有一种网上的推荐方式是设置初始值,然后使用 typeof 拿到类型(可能会给其他地方用)。也可以使用类型断言可以解决这类问题:

interface User { 
    name: string; 
    age: number; 
} 

export default class someClass { 
    private user = {} as User;
} 

枚举类型分为数字类型与字符串类型,其中数字类型的枚举可以当标志使用:

enum AnimalFlags {
    None = 0, 
    HasClaws = 1 << 0, 
    CanFly = 1 << 1, 
    HasClawsOrCanFly = HasClaws | CanFly 
}

interface Animal { 
    flags: AnimalFlags; 
   [key: string]: any; 
} 

function printAnimalAbilities(animal: Animal) { 
    var animalFlags = animal.flags; 
    if (animalFlags & AnimalFlags.HasClaws) { 
        console.log('animal has claws'); 
    } 
    if (animalFlags & AnimalFlags.CanFly) { 
        console.log('animal can fly'); 
    } 
    if (animalFlags == AnimalFlags.None) { 
        console.log('nothing'); 
    } 
} 

var animal = { flags: AnimalFlags.None }; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // animal has claws 
animal.flags &= ~AnimalFlags.HasClaws; 
printAnimalAbilities(animal); // nothing 
animal.flags |= AnimalFlags.HasClaws | AnimalFlags.CanFly; 
printAnimalAbilities(animal); // animal has claws, animal can fly 
  • 使用 |= 来添加一个标志;
  • 组合使用 &= 和 ~ 来清理一个标志;
  • | 来合并标志。

这个或许不常用,在 typescript 关于 types 源码中我们也可以看到类似的代码: types

字符串类型的枚举可以维护常量:

const enum TODO_STATUS {
  TODO = 'TODO',
  DONE = 'DONE',
  DOING = 'DOING'
}

function todos (status: TODO_STATUS): Todo[];

todos(TODO_STATUS.TODO)

表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number];
x = ['hello', 10]; 

在发出不固定多个请求时,可以应用:

const requestList: any[] = [http.get<A>('http://some.1')]; // 设置为 any[] 类型 
if (flag) { 
    requestList[1] = (http.get<B>('http://some.2')); 
} 
const [ { data: a }, response ] = await Promise.all(requestList) as [Response<A>, Response<B>?]

在定义泛型后,有两种方式使用,一种是传入泛型类型,另一种使用类型推断。

declare function fn<T>(arg: T): T; // 定义一个泛型函数 
const fn1 = fn<string>('hello'); // 第一种方式,传入泛型类型 
string const fn2 = fn(1); // 第二种方式,从参数 arg 传入的类型 number,来推断出泛型 T 的类型是 number 

一个扁平数组结构建树形结构例子:

// 转换前数据 
const arr = [ 
{ id: 1, parentId: 0, name: 'test1'}, 
{ id: 2, parentId: 1, name: 'test2'}, 
{ id: 3, parentId: 0, name: 'test3'} 
]; 
// 转化后 
[ { id: 1, parentId: 0, name: 'test1', 
    childrenList: [ { id: 2, parentId: 1, name: 'test2', childrenList: [] } ] }, 
    { id: 3, parentId: 0, name: 'test3', childrenList: [] } 
]


interface Item { 
    id: number; 
    parentId: number; 
    name: string; 
} 

// 传入的 options 参数中,得到 childrenKey 的类型,然后再传给 TreeItem

interface Options<T extends string> { 
    childrenKey: T; 
} 
type TreeItem<T extends string> = Item & { [key in T]: TreeItem<T>[] | [] }; 
declare function listToTree<T extends string = 'children'>(list: Item[], options: Options<T>): TreeItem<T>[]; 
listToTree(arr, { childrenKey: 'childrenList' }).forEach(i => i.childrenList) 

infer

表示在 extends 条件语句中待推断的类型变量。

type ParamType<T> = T extends (param: infer P) => any ? P : T; 

这句话的意思是:如果 T 能赋值给 (param: infer P) => any,则结果是 (param: infer P) => any 类型中的参数 P,否则返回为 T。

interface User { 
    name: string; 
    age: number; 
} 
type Func = (user: User) => void 
type Param = ParamType<Func>; // Param = User 
type AA = ParamType<string>; // string

// [string, number] -> string | number
type ElementOf<T> = T extends Array<infer E> ? E : never;

type TTuple = [string, number];

type ToUnion = ElementOf<TTuple>; // string | number


// T1 | T2 -> T1 & T2
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends ((k: infer I) => void) ? I : never;

type Result = UnionToIntersection<T1 | T2>; // T1 & T2

typescript 关于类型限制还是非常强大的,由于文章有限,还有其他类型比如联合类型,交叉类型等读者可自行翻阅资料查看。刚开始接触范型以及其各种组合会感觉不熟练,接下来在项目中会慢慢应用,争取将 bug 降至最低限度。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK