◆

TypeScript 常用基础类型详解

2023-02-02 01:370

boolean

let isDone: boolean = false;

注意,使用构造函数 Boolean 创造的对象不是布尔值:

/**
 * TS2322: Type 'Boolean' is not assignable to type 'boolean'.
 * 'boolean' is a primitive, but 'Boolean' is a wrapper object.
 * Prefer using 'boolean' when possible
 */
let createdByNewBoolean: boolean = new Boolean(1);

直接调用 Boolean 返回一个 boolean 类型:

let createdByBoolean: boolean = Boolean(1);

null && undefined

  • 在 TypeScript 中,可以使用 null 和 undefined 来定义这两个原始数据类型
  • 与 void 的区别是,undefined 和 null 是所有类型的子类型。也就是说 undefined 类型的变量,可以赋值给 number 类型的变量:
let num: number;
let str: string;

// 这些类型能被赋予
num = null;
str = undefined;

// 以下不会报错
let nums: number = undefined;
let u: undefined;
let numss: number = u;

// 而 void 类型的变量不能赋值给 number 类型的变量:
let u1: void;
// Type 'void' is not assignable to type 'number'.
let num1: number = u;

tip

tsconfig.json:strict 属性值是否为 false 或者 true 会影响上面的值

void

声明一个 void 类型的变量没有什么大用,因为你只能为它赋予 undefined 和 null

let unusable: void = undefined;

void && undefined

// Parent.tsx
function Parent(): JSX.Element {
  // 这里函数返回的是number类型
  const getValue = (): number => {
    return 2;
  };
  // 这里函数返回的string类型,同样可以传给子属性
  // const getValue = (): string => { return 'str' };
  return <Child getValue={getValue} />;
}

// Child.tsx
type Props = {
  // 这里的 void 表示逻辑上不关注具体的返回值类型,number、string、undefined 等都可以
  getValue: () => void;
};

function Child({ getValue }: Props) {
  return <div>{getValue()}</div>;
}

unknown

function test(input: unknown): number {
  if (Array.isArray(input)) {
    // Pass: 这个代码块中,类型守卫已经将input识别为array类型
    return input.length;
  }
  /**
   * Error: 这里的 input 还是 unknown 类型,静态检查报错。如果入参是 any,则会放弃检查直接成功,带来报错风险
   * TS2339: Property 'length' does not exist on type 'unknown'.
   */
  return input.length;
}

never

never 是指没法正常结束返回的类型,一个必定会报错或者死循环的函数会返回这样的类型。

// throw error 返回值是never
function foo(): never {
  throw new Error('error message');
}

// 这个死循环的也会无法正常退出
function foo(): never {
  while (true) {}
}

// Error: 这个无法将返回值定义为never,因为无法在静态编译阶段直接识别出
function foo(): never {
  let count = 1;
  while (count) {
    count++;
  }
}

无法把其他类型赋给 never。

let n: never;
let o: any = {};
// Error: 不能把一个非 never 类型赋值给 never 类型,包括 any
n = o;

数组定义的一些方式

let list: number[] = [1, 2, 3];
let list1: Array<number> = [1, 2, 3];
let list2: any[] = [1, true, 'free'];

类型断言

/** 方式一 */
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
/** 方式二 在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的。 */
let someValue1: any = 'this is a string';
let strLength1: number = (someValue1 as string).length;

带有任意数量的其它属性

[propName: number]: number|string|Array<any>; :必须包含前面其他对象 value 的类型

tip

一旦定义了任意属性,那么确定属性和可选属性的类型都必须是它的类型的子集

type a = {
  a: 1;
  b: 2;
  [x: string]: number;
};

interface SquareConfig {
  color: string;
  width: number;
  test: Array<any>;

  [propName: number]: number | string | Array<any>;

  // TS2413: Numeric index type 'string | number | any[]' is not assignable to string index type 'string | number'.
  // [key: string]: number|string;
}

接口

readonly

一些对象属性只能在对象刚刚创建的时候修改其值。 可以在属性名前用 readonly 来指定只读属性

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

TypeScript 具有 ReadonlyArray<T> 类型,它与 Array<T> 相似, 只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

上面代码的最后一行,可以看到就算把整个 ReadonlyArray 赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写

a = ro as number[];

额外的属性检查

ts 会提示报错 error: 'colour' not expected in type 'SquareConfig'

interface SquareConfig {
  color?: string;
  width?: number;
}

function createSquare(config: SquareConfig): { color: string; area: number } {
  // ...
}

let mySquare = createSquare({ colour: 'red', width: 100 });
  • 解决方法:如果 SquareConfig 带有上面定义的类型的 color 和 width 属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
interface SquareConfig {
  color?: string;
  width?: number;

  [propName: string]: any;
}

函数类型

interface SearchFunc {
  // 函数的参数 (source,subString) 函数的返回值: boolean
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
};

/** 对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 */
let mySearchOne: SearchFunc;
mySearchOne = function (src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
};

/**
 * 函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。
 */
let mySearchTwo: SearchFunc;
mySearchTwo = function (src, sub) {
  let result = src.search(sub);
  return result > -1;
};

混合类型

一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
  (start: number): string;

  interval: number;

  reset(): void;
}

function getCounter(): Counter {
  let counter = <Counter>function (start: number) {};
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

剩余参数

function buildName(firstName: string, ...restOfName: string[]) {
  return firstName + ' ' + restOfName.join(' ');
}

let employeeName = buildName('Joseph', 'Samuel', 'Lucas', 'MacKinzie');

泛型

下面来创建第一个使用泛型的例子:identity 函数。 这个函数会返回任何传入它的值。 不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
  return arg;
}

或者,我们使用 any 类型来定义函数:

function identity(arg: any): any {
  return arg;
}

使用 any 类型会导致这个函数可以接收任何类型的 arg 参数, 这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。 如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 **类型变量 **,它是一种特殊的变量,只用于表示类型而不是值。

function identity<T>(arg: T): T {
  return arg;
}
  • 我们给 identity 添加了类型变量 T。 T 帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了 T 当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

  • 我们把这个版本的 identity 函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any,它不会丢失信息, 像第一个例子那像保持准确性,传入数值类型并返回数值类型。

定义了泛型函数后,可以用两种方法使用。

  • 第一种是,传入所有的参数,包含类型参数:
/** 这里我们明确的指定了T是string类型,并做为一个参数传给函数,使用了<>括起来而不是()。 */
let output = identity<string>('myString'); // type of output will be 'string'
  • 第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定 T 的类型:
let output = identity('myString'); // type of output will be 'string'

示例

function loggingIdentity<T>(arg: T[]): T[] {
  // Array has a .length, so no more error
  console.log(arg.length);
  return arg;
}
function loggingIdentity<T>(arg: Array<T>): Array<T> {
  // Array has a .length, so no more error
  console.log(arg.length);
  return arg;
}
// 普通类型定义
type Dog<T> = { name: string; type: T };
// 普通类型使用
const dog: Dog<number> = { name: 'ww', type: 20 };

// 类定义
class Cat<T> {
  private type: T;

  constructor(type: T) {
    this.type = type;
  }
}

// 类使用
const cat: Cat<number> = new Cat<number>(20); // 或简写 const cat = new Cat(20)

// 函数定义
function swipe<T, U>(value: [T, U]): [U, T] {
  return [value[1], value[0]];
}

// 函数使用
swipe<Cat<number>, Dog<number>>([cat, dog]); // 或简写 swipe([cat, dog])

枚举

  • 数字枚举:当没有给枚举定值时,默认 X = 0, Y = 1, Z = 2
  • 当 A = 1 时,后面自增长, B = 2, C = 3
enum E1 {
  X,
  Y,
  Z,
}

enum E2 {
  A = 1,
  B,
  C,
}
/** 增长示例 */
enum E {
  A = 1,
  B = 100,
  C,
  D,
  E = 200,
  F,
}
常用的基础类型

tip

当其中某个枚举没赋予具体值时,会自动识别为数值。
但当除第一个枚举外,其他枚举要想自动识别为数值,要保证其上一个枚举是一个数值, 或者上一个枚举已经被自动识别成了数值。

enum E {
  a,
  b = '中国',
  c = 100,
  d,
  e,
  f = '103',
  g = 104,
  h,
}

联合类型

在 JavaScript 中,你可能希望属性为多种类型之一,如字符串或者数组。这正是 TypeScript 中联合类型能派上用场的地方 (它使用 | 作为标记,如 string | number)。关于联合类型,一个常见的用例是一个可以接受字符串数组或单个字符串的函数

function formatCommandline(command: string[] | string) {
  let line = '';
  if (typeof command === 'string') {
    line = command.trim();
  } else {
    line = command.join(' ').trim();
  }
}
export type ButtonSize = 'lg' | 'md' | 'sm';

keyof

用于获取某种类型的所有键,其返回类型是 联合类型

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

const test: Person = { name: '11', age: 11, location: '22' };
type K1 = keyof Person; // "name" | "age" | "location"
const tests: K1 = 'name';

const test1: Person[] = [{ name: '11', age: 11, location: '22' }];
/** 为什么会有 number 是否是继承造成的 */
type K2 = keyof Person[]; // number | "length" | "push" | "concat" | ...
/** type K2s = keyof []; // 等同于 K2 */
type K2s = keyof []; // number | "length" | "push" | "concat" | ...
const test1s: K2 = 10 || 'length' || 'push';
const test2: { [x: string]: Person } = {
  1: { name: '11', age: 11, location: '22' },
};

// 索引签名参数类型必须为 "string" 或 "number"。
type K3 = keyof { [x: string]: Person }; // string | number
const test2s: K3 = 10 || '333';

type K4 = keyof boolean;
const test3: K4 = 'valueOf';

let K2: keyof number = 'valueOf' || 'toFixed';

// 期望用户输入的属性是对象上已存在的属性
function prop(obj, key) {
  return obj[key];
}

function propTs(obj: object, key: string) {
  return obj[key];
}

function propTss<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

console.log(propTss(['a'], 0));
console.log(propTss(['a'], 1));
  • prop 函数的作用是用于获取某个对象中指定属性的属性值。 因此我们期望用户输入的属性是对象上已存在的属性, 那么如何限制属性名的范围呢?可以利用 keyof 操作符:
  • 在以下代码中,我们使用了 TypeScript 的泛型和泛型约束。
  • 首先定义了 T 类型并使用 extends 关键字约束该类型必须是 object 类型的子类型,
  • 然后使用 keyof 操作符获取 T 类型的所有键,其返回类型是联合类型,
  • 最后利用 extends 关键字约束 K 类型必须为 keyof T 联合类型的子类型
function prop<T extends object, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

穿插类型

type TestOne = {
  a: string;
};

type TestTwo = {
  b: string;
};

type TestThree = {
  c?: string;
};

const myTest: TestOne & TestTwo & TestThree = { a: '1', b: '2' };
const myTest1: TestOne & TestTwo & TestThree = { a: '1', b: '2', c: '3' };
/** error: d 不包含在 TestOne & TestTwo & TestThree 类型中 */
const myTest2: TestOne & TestTwo & TestThree = { a: '1', b: '2', c: '3', d: 4 };

/** error: b 在 TestOne & TestTwo & TestThree 是必须拥有的 */
const myTest3: TestOne & TestTwo & TestThree = { a: '1' };

extends

interface TestOnes {
  a: string;
}

interface TestTwos extends TestOnes {
  d?: string;
}

const myTests: TestTwos = { a: '1', d: '2' };

当联合类型无法做出判断时 我们编写一个 函数类型,逻辑很简单:给的类型是 x 时返回 a 则返回 b。

type AB<T> = T extends 'x' ? 'a' : 'b';

type A = AB<'x'>;
// 得到 type A = 'a'

看上去没什么问题,那是因为我们传入了一个明确的判断值,肯定为 true。 假设我们传入不确定的值,例如一个联合类型 'x' | 'y' 会怎么样呢?判断逻辑可能是 true,也可能是 false。 其实 TypeScript 也不知道该怎么办,于是乎它就把两个结果的值都返回给我们了:

type AB<T> = T extends 'x' ? 'a' : 'b';

type All = AB<'x' | 'y'>; // 非确定条件,可能是 'x' 或 'y'
// 得到 type All = 'a' | 'b';

我们得到了一个 联合类型 包含所有返回值的。 官方的解释是:此时做了 推迟解析条件类型 的处理。

推迟解析条件类型的额外效果

type Other = 'a' | 'b';
type Merge<T> = T extends 'x' ? T : Other; // T 等于匹配的类型,然后加上 Other 联合类型一起返回

type Values = Merge<'x' | 'y'>;
// 得到 type Values = "x" | "a" | "b"; 当为 x 返回 x, 当为 y 时返回 Other

利用此特性写的 Filter 类型定义

type Filter<T, U> = T extends U ? never : T;
type Values = Filter<'x' | 'y' | 'z', 'x'>;
// 得到 type Values = "y" | "z"

Partial

将某个类型里的属性全副变为可选项。

// 实现:全副变可选
type Partial<T> = {
  [P in keyof T]?: T[P];
};

例子

interface Animal {
  canFly: boolean;
  canSwim: boolean;
}

// 变可选,能够只赋值局部属性
let animal: Partial<Animal> = {
  canFly: false,
};

ReadOnly

它接管一个泛型 T,用来把它的所有属性标记为只读类型

// 实现:全副变只读
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

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

let person: Readonly<Person> = {
  name: 'jacky',
  age: 24,
};
person.name = 'jack'; // Cannot assign to 'name' because it is a read-only property.

Required

将某个类型里的属性全副变为必选项。

// 实现:全副变必选
type Required<T> = {
  [P in keyof T]-?: T[P];
};

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

// Property 'age' is missing in type '{ name: string; }' but required in type 'Required<Person>'.
let person: Required<Person> = {
  name: 'jacky',
  // 没写 age 属性会提醒谬误
};

Record

// 实现:K 中所有属性值转化为 T 类型
type Record<K extends keyof any, T> = {
  [P in K]: T;
};

// Record 生成的类型具备类型 K 中存在的属性,值为类型 T
interface DatabaseInfo {
  id: string;
}

type DataSource = 'user' | 'detail' | 'list';

const x: Record<DataSource, DatabaseInfo> = {
  user: { id: '1' },
  detail: { id: '2' },
  list: { id: '3' },
};

示例

/** 对象的值类型 */
interface DatabaseInfo {
  id: string;
}

/** 对象的键类型 */
type DataSource = string;
type RecordTest = Record<DataSource, DatabaseInfo>;

const x: RecordTest = {
  false: { id: '1' },
  undefined: { id: '2' },
  null: { id: '3' },
};

const x1: {
  user: { id: string };
  detail: { id: string };
  list: { id: string };
} = {
  user: { id: '1' },
  detail: { id: '2' },
  list: { id: '3' },
};

type EventType = string;
type RecordUnknown = Record<EventType, unknown>;

const test: RecordUnknown = {
  Array: [],
  a: 1,
  b: 2,
  c: 3,
  1: [],
};

Exclude

// 实现:如果 T 中的类型在 U 不存在,则返回,否则不返回
type Exclude<T, U> = T extends U ? never : T;

// 将某个类型中属于另一个的类型移除掉
interface Programmer {
  name: string;
  age: number;
  isWork: boolean;
  isStudy: boolean;
}

interface Student {
  name: string;
  age: number;
  isStudy: boolean;
}

type ExcludeKeys = Exclude<keyof Programmer, keyof Student>;
// type ExcludeKeys = "isWork"

Omit

// 实现:去除类型 T 中蕴含 K 的键值对。
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

// 作用于 Pick 相同,还是间接看代码容易了解
interface Animal {
  canFly: boolean;
  canSwim: boolean;
}

let person1: Pick<Animal, 'canSwim'> = {
  canSwim: true,
};

let person2: Omit<Animal, 'canFly'> = {
  canSwim: true,
};

ReturnType

// 实现:获取 T 类型(函数)对应的返回值类型
type ReturnType<T extends (...args: any) => any> = T extends (
  ...args: any
) => infer R
  ? R
  : any;

// 获取函数返回值类型
function bar(x: string | number): string | number {
  return 'hello';
}

// string | number
type FooType = ReturnType<typeof bar>;

遍历属性 in

in 只能用在类型的定义中,可以对枚举类型进行遍历 keyof 返回泛型 T 的所有键枚举类型,key 是自定义的任何变量名,中间用 in 链接,外围用[]包裹起来(这个是固定搭配), 冒号右侧 number 将所有的 key 定义为 number 类型。 总结起来 in 的语法格式如下:

[ 自定义变量名 in 枚举类型 ]: 类型

示例如下

type DynamicProps<T> = {
  [P in keyof T]: Array<T[P]> | T[P];
};

type Person = {
  name: string;
  age: number;
};

/**
 * @description 这个类型可以将任何类型的键值转化成number类型
 * @author wfd
 * @date 2020/10/9 13:30
 * @example 相当于将 T (假设 T 是对象) 通过 keyof T 将会把 T 的每一个键拿出来
 [key in keyof T] ===> 相当于将 每一个键遍历出来
 最后形成如下:
 {
 	 		T 对象中的 key 值 1: number,
 	 		T 对象中的 key 值 2: number,
 	 }
 */
type TypeToNumber<T> = {
  [key in keyof T]: number;
};

/**
 * @description
 TypeToNumber<Person> 相当于转换成了
 {
 	 		name: number
 	 		age: number
 	 }
 * @author wfd
 * @date 2020/10/9 13:28
 */
const obj: TypeToNumber<Person> = { name: 10, age: 10 };
const obj1: { name: number; age: number } = { name: 10, age: 10 };

/** 直接用 Person 类型则 name 属性无法用字符串,因为 Person 中的 name 属性的类型是 string */
const obj2: Person = { name: 10, age: 10 };

/** 下面这种实现的效果将会和 Person 一致 */
type TypeOne<T> = {
  [P in keyof T]: T[P];
};

const objTwo: TypeOne<Person> = { name: '10', age: 10 };
const objTwos: { name: string; age: number } = { name: '10', age: 10 };

/**
 * @description 相当于转换成了如下形式
 {
 	 		T 对象中的 key 值 1: Array<T 对象中的第一个 value 对应的类型>,
 	 		T 对象中的 key 值 1: Array<T 对象中的第二个 value 对应的类型>,
 	 }
 * @author wfd
 * @date 2020/10/9 13:37
 */
type TypeTwo<T> = {
  [P in keyof T]: Array<T[P]>;
};

/**
 * @description TypeTwo<Person> 相当于转换成了
 {name: Array<string>, age: Array<number>}
 * @author wfd
 * @date 2020/10/9 13:38
 */
const objThree: TypeTwo<Person> = { name: ['10'], age: [10] };
const objThrees: { name: Array<string>; age: Array<number> } = {
  name: ['10'],
  age: [10],
};

使用 interface 还是 type 来定义类型

从用法上来说两者本质上没有区别,大家使用 React 项目做业务开发的话,主要就是用来定义 Props 以及接口数据类型。

但是从扩展的角度来说,type 比 interface 更方便拓展一些,假如有以下两个定义:

type Name = { name: string };

interface IName {
  name: string;
}

想要做类型的扩展的话,type 只需要一个&,而 interface 要多写不少代码。

type Person = Name & { age: number };

interface IPerson extends IName {
  age: number;
}

另外 type 有一些 interface 做不到的事情,比如使用|进行枚举类型的组合,使用 typeof 获取定义的类型等等。

不过 interface 有一个比较强大的地方就是可以重复定义添加属性,比如我们需要给 window 对象添加一个自定义的属性或者方法,那么我们直接基于其 Interface 新增属性就可以了。

declare global {
  interface Window {
    MyNamespace: any;
  }
}

总体来说,大家知道 TS 是类型兼容而不是类型名称匹配的,所以一般不需用面向对象的场景或者不需要修改全局类型的场合,我一般都是用 type 来定义类型。

💬留言板0

还没有留言,来发表第一条吧~

PAGE LOAD / LIVETypeScript 常用基础类型详解加载中