Skip to content
☆´∀`☆
On this page

数据类型

基础类型

  • string
  • number
  • boolean
  • array number[]
  • 元组 [number,string]
  • undefined
  • null
  • any
  • unknown
  • void 用在没返回的函数上
  • never (抛出异常 & 无限循环代码)

层级关系

  • Top Type: any & unknown
  • 特殊的 Object ,它也包含了所有的类型,但和 Top Type 比还是差了一层
  • String、Boolean、Number 这些装箱类型
  • 原始类型与对象类型
  • 字面量类型,即更精确的原始类型与对象类型嘛,需要注意的是 null 和 undefined 并不是字面量类型的子类型
  • never 类型

unknown

unknown 是 TypeScript 3.0 引入的新类型,它的作用是表示未知类型的值。任何类型的值都可以赋值给 unknown 类型,但是 unknown 类型的值不能直接赋值给其他类型,只能赋值给 unknown 类型或者 any 类型。

ts
let value: unknown
value = true
value = 1
value = 'hello'
value = []
value = {}
value = Math.random
value = null
value = undefined

const val1: string = value // Error
const val2: number = value // Error
const val3: () => {} = value // Error
const val4: {} = value // Error

const val5: any = value
const val6: unknown = value

要对 unknown 类型进行属性访问,需要进行类型断言。

ts
let unknownVar: unknown;
(unknownVar as { foo: () => {} }).foo()

never

never 表示永远不会返回的类型(什么都没有),它可以是抛出异常的函数,也可以是无限循环的函数。

never 类型不携带任何的类型信息,因此会在联合类型中被直接移除。

ts
type UnionWithNever = 'foo' | 599 | true | void | never;
// 这里显示的类型是"foo" | 599 | true | void

never 是整个类型系统里最底层的类型。它是所有类的子类型,但只有 never 类型的变量能够赋值给另一个 never 类型的变量。

通常,never 用于抛出异常或者无法执行到的终点。

ts
function error (message: string): never {
  throw new Error(message)
}

也可以用作于类型检查

比如,一开始有一个类型为 string | number,需要对不同类型进行不同处理:

ts
declare const strOrNum: string | number
if (typeof strOrNum === 'string') console.log('str!')
else if (typeof strOrNum === 'number') console.log('num!')
else throw new Error(`Unknown input type: ${strOrNum}`)

后来,这个类型变成了 string | number | boolean,但是我们忘记了修改这里的代码,这时候就会报错。这时候,可以用 never 来帮助类型检查:

ts
if (typeof strOrNum === 'string'){
  console.log('str!')
} else if (typeof strOrNum === 'number'){
  console.log('num!')
} else {
  const _exhaustiveCheck: never = strOrNum
  throw new Error(`Unknown input type: ${_exhaustiveCheck}`)
}

因为,由于 TypeScript 强大的类型分析能力,每经过一个 if 语句处理,strOrNum 的类型分支就会减少一个(因为已经被对应的 typeof 处理过)。而在最后的 else 代码块中,它的类型只剩下了 never 类型。

数组

数组声明

ts
const arr1: number[] = [1, 2, 3]
const arr2: Array<number> = [1, 2, 3]

以上两种声明方式是等价的,但是推荐使用第一种。

元组

元组是固定长度的数组,每个元素的类型可以不同。

ts
const tuple: [string, number] = ['hello', 1]

在越界访问时,会报错。

ts
const tuple: [string, number] = ['hello', 1]
tuple[2] // error

元组可以提升数组结构的严谨性,避免出现越界访问。

具名元组

TypeScript 4.0 中,可以给元组中的每个元素定义一个名字。

ts
const tuple: [name: string, age: number] = ['hello', 1]
tuple[0] // 'hello'

对象

  • object 只能赋值{}
  • Object & {} (代表所有拥有 toString、hasOwnProperty 方法的类型 所以所有原始类型、非原始类型都可以赋给 Object)
typescript
let bigObject: Object;
object = 1; // 编译正确
object = "a"; // 编译正确
object = true; // 编译正确
object = null; // 报错
ObjectCase = undefined; // 报错
ObjectCase = {}; // ok

interface 接口

通过 interface 关键字定义接口,用来描述对象类型。

typescript
interface Person {
  name: string;
  age: number;
  [prop: string]: any; // 其他的任意属性
}

可选

通过在属性名后面加上?来表示可选属性。

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

只读

通过在属性名前面加上readonly来表示只读属性。

typescript
interface Person {
  readonly idNum: number;
  name: string;
  age?: number;
}

继承 extends

接口可以继承多个接口,用,分隔。

typescript
interface Person {
  name: string;
  age?: number;
}
interface Girl {
  shoes: string;
}
interface Student extends Person, Girl {
  school: string;
}

enum 枚举

枚举可以拥有更好的类型提示,并且这些常量会被约束在一个命名空间下。

typescript
enum API_URL {
  ADD_ADMIN = "XXXXX",
  EDIT_ADMIN = "XXXX",
  DELETE_ADMIN = "XXXX",
}
const xx = API_URL.ADD_ADMIN;

如果没有声明初始值,那么默认值为 0,后续的值会自动加 1。

typescript
enum Color {
  RED,
  BLUE,
  GREEN,
}
const myBookColor = Color.BLUE; // 1

使用计算值

只有数值枚举可以使用计算值。

typescript
const returnUrl = (type: number) => type * 2;
enum API_URL {
  ADD_ADMIN = returnUrl(0),
  EDIT_ADMIN = returnUrl(3),
  DELETE_ADMIN = returnUrl(4),
}
const xx = API_URL.ADD_ADMIN; // 0

双向映射

可以从枚举值到枚举名字,也可以从枚举名字到枚举值。

仅有值为数字的枚举成员才能够进行这样的双向枚举

typescript
enum Color {
  RED,
  BLUE,
  GREEN,
}
const myBookColor1 = Color.BLUE; // 1
const myBookColor2 = Color[2]; // 1

常量枚举

带 const 在编辑阶段删除,只能通过枚举成员访问枚举值。

typescript
const enum Color {
  RED = "red",
  BLUE = "blue",
  GREEN = "green",
}
const myBookColor: Color = Color.BLUE;

函数

函数声明

typescript
function add(x: number, y?: number): number {
  return y ? x + y : x;
}

箭头函数

typescript
// 方式一
const foo = (name: string): number => {
  return name.length;
};

// 方式二
const foo: (name: string) => number = name => {
  return name.length;
};

方式二的可读性较差,推荐以方式一。或者用类型别名将函数声明抽离出来。

typescript
type Foo = (name: string) => number;
const foo: Foo = name => {
  return name.length;
};

void 和 underfined

  • void 用在没返回的函数上
  • underfined 用在有返回的但没有返回值的函数上
typescript
function foo(): void {
  console.log("foo");
}
function bar(): undefined {
  console.log("bar");
  return;
}

可选参数 和 rest 参数

typescript
function foo(name: string, age?: number, ...rest: string[]): void {
  console.log(name, age, rest);
}
foo("hello", 1, "a", "b", "c");

可选参数必须位于必选参数之后。带默认值的参数也可以看做是可选参数。

typescript
function foo(name: string, age: number = 18): void {
  console.log(name, age);
}
foo("hello");

函数重载 Overload Signature

在逻辑比较复杂的情况下,函数可能会有多组入参类型和返回值类型。这时候就可以使用函数重载。

typescript
// 重载1 flag为true时返回string
function func(foo: number, flag: true): string;
// 重载2 flag为false时返回number
function func(foo: number, flag?: false): number;
// 函数的实现签名
function func(foo: number, flag?: boolean): string | number {
  if (flag) return String(foo);
  return foo * 599;
}

Generator 函数

typescript
function* foo(): Generator<void> {}
function* bar(): Generator<number> {
  yield 1;
  yield 2;
  yield 3;
}
const a = bar();
console.log(a.next()); // { value: 1, done: false }
console.log(a.next()); // { value: 2, done: false }
console.log(a.next()); // { value: 3, done: false }
console.log(a.next()); // { value: undefined, done: true }

async 函数

async函数返回的必是一个Promise对象。

typescript
async function foo(): Promise<void> {}
async function bar(): Promise<number> {
  return 1;
}

Class 的主要结构只有构造函数、属性、方法、访问符(Accessor)。

修饰符

在 typescript 中,修饰符有三种:publicprivateprotectedreadonly

  • public:默认修饰符,可以在类的实例子类中访问。
  • private:只能在中内部访问。
  • protected:只能在子类中访问。只能被继承而不能被实例化,通常用于定义抽象基类。

静态成员

使用static关键字来标志一个静态成员。静态成员只能通过类名访问,不能通过实例访问。

typescript
class Person {
  static names = "hello";
  static getName(): string {
    return Person.names;
  }
}
class Student extends Person {
  age: number = 18;
}
const y = new Student();
console.log(y.age); // 18
console.log(Person.getName()); // hello
console.log(Student.getName()); // hello

在静态方法中,this 指向的是类本身,所以只能访问同为静态的成员。

静态成员直接挂载上函数体上,而实例成员挂载在原型上(ES5 及以下,ES6 原生支持 static)。

静态成员不会被实例继承,但是可以被子类继承。

可以用类+静态成员来实现命名空间的功能。

typescript
class Person {
  static name: string = "hello";
  static getName() {
    return Person.name;
  }
}
class Student extends Person {
  age: number = 18;
}
console.log(Person.name); // hello
console.log(Person.getName()); // hello
console.log(Student.name); // hello
console.log(Student.getName()); // hello

override 覆盖方法

override来确保子类覆盖的方法在父类中存在。

typescript
class Base {
  foo: string = "foo";
  print() {
    console.log(Base.name);
  }
}

class Derived extends Base {
  override print() {
    console.log(Derived.name);
  }

  // error :This member cannot have an 'override' modifier because it is not declared in the base class 'Base'
  override xxx() {}
}

抽象类

使用abstract关键字来定义抽象类,抽象类不能被实例化,只能被继承。

typescript
abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log("roaming the earch...");
  }
}
class Cat implements Animal {
  makeSound() {
    console.log("喵喵喵");
  }
  move() {
    console.log("move move");
  }
}
const mm = new Cat();
mm.move();

在 ts 中,无法声明静态的抽象成员。

<> as 类型断言

typescript
let str: any = "to be or not to be";
let strLength: number = (<string>str).length;

let str: any = "to be or not to be";
let strLength: number = (str as string).length;

推荐使用as来获取更好的阅读体验。

双重断言

原类型与断言类型之间差异过大(指鹿为马)时,ts 会报错,这个时候需要双重断言。

先断言到unknown类型,再断言到目标类型。

typescript
const str: string = "xxxx";
(str as unknown as { handler: () => {} }).handler();

! 非空断言

使用!形式标记前面的声明一定是非空的。

typescript
let user: string | null | undefined;
console.log(user!.toUpperCase()); // 编译正确
console.log(user.toUpperCase()); // 错误

| 联合类型

typescript
let status: string | number;
status = "to be or not to be";
status = 1;

& 交叉类型

表示两个类型都必须存在

code
ts
interface IpersonA {
  name: string
  age: number
}
interface IpersonB {
  name: string
  gender: string
}

const person: IpersonA & IpersonB = {
  name: '师爷',
  age: 18,
  gender: ''
}

type UnionIntersection1 = (1 | 2 | 3) & (1 | 2); // 1 | 2
type UnionIntersection2 = (string | number | symbol) & string; // string

如果交叉类型中相同的 key,但是属性修饰符不同时

  • name & name? = name
    code
ts
interface A {
  name: string
}
interface B {
  name?: string
}
type AB = A & B;
type BA = B & A;

const b: B = {}
const ab: AB = {
  name: 'xxx'
}
const ba: BA = {
  name: 'xxx'
}

:::

  • name & readonly name = name
  • ?name & readonly name = name
code
ts
interface A {
  name?: string
}
interface B {
  readonly name: string
}

type AB = A & B;

type BA = B & A;

const ab: AB = {
  name: 'xxx'
}
const ba: BA = {
  name: 'xxx'
}

ab.name = '1'
ba.name = '1'

交叉类型中的 key 名称相同但是类型不同,则该 key 为 never 类型

type 类型别名

类型别名用来给一个类型起个新名字。它只是起了一个新名字,并没有创建新类型。

它的主要作用是对一组类型或一个特定结构进行封装,方便重用。

抽离一组联合类型

ts
type StatusCode = 200 | 301 | 400 | 500 | 502;
type MaybeNull<T> = T | null;

抽离一个函数类型

ts
type handlerMouseEvent = (e: MouseEvent) => void;
const handleClick: handlerMouseEvent = (e) => {}

它可以接收泛型,变为更灵活的创建工具

ts
type MaybeArray<T> = T | T[];
function ensureArray<T> (input: MaybeArray<T>): T[] {
  return Array.isArray(input) ? input : [input]
}
  • type 有时和 interface 很像,但是可以作用于原始值(基本类型),联合类型,元组以及其它任何你需要手写的类型。起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型
  • 可以拓展
  • 可以声明基本数据类型别名/联合类型/元组等

[]: 索引签名类型

索引签名类型主要用在 interface 和 type 中,用来快速声明一个键值类型一致的类型结构。

ts
interface Base {
  [key: string]: string
}

但是在 js 中,obj[prop]形式的访问会将数字索引转换为字符串索引访问,所以在字符串索引签名类型中,仍然可以声明数字和 symbol 类型的键名。

ts
interface Base {
  [key: string]: string
}

const obj: Base = {
  xx: 'xxx',
  111: 'foo',
  [Symbol('a')]: 'xxx'
}

keyof 索引类型查询操作符

keyof可以将对象中的所有键转为对应字面量的联合类型。

注意,这里并不会将数字类型的键名转换为字符串类型字面量,而是仍然保持为数字类型字面量。

ts
interface Base {
  age: number
  username: string
}

type BaseKeys = keyof Base;

const key: BaseKeys = 'age'

索引类型访问

ts
interface Base {
  age: number
  username: string
}

type Age = Base['age']; // number

也可以和 keyof 结合使用

ts
interface Base {
  age: number
  username: string
}

type BaseValues = Base[keyof Base]; // string | number

泛型

在定义函数、接口、类的时候,先不指定具体的类型。而在使用的时候再指定一种类型。

typescript
function getValue<T>(x: T): T {
  return x;
}

getValue<string>("yomuki"); // 手动定义
getValue("yomuki"); // 自动推断

泛型约束

用 extend 约束这个泛型必须存在某些类型

typescript
interface Persom {
  readonly idNum: number; // 只读
  age?: number; // 可选
  name: string;
  [prop: string]: any; // 其他的任意属性
}

function getValue<T extends Persom>(x: T): T {
  return x;
}

getValue({
  idNum: 123,
  name: "yOMUKI",
});

泛型接口

typescript
interface Jk<T, U> {
  name: T;
  age: U;
  car: string[];
}
const jk1: Jk<string, number> = {
  name: "name",
  age: 123,
  car: [],
};

内置方法中的泛型

Promise

ts
interface IBase {
  name: string
}
const p = new Promise<IBase>((resolve) => {
  resolve({
    name: 'xxx'
  })
})

p.then((res) => {
  console.log(res.name)
})

Array.prototype.reduce

ts
const arr: number[] = [1, 2, 3, 4]
const res = arr.reduce<number[]>((prev, cur) => {
  prev.push(cur)
  return prev
}, [])

in 映射类型

映射类型的主要作用是基于键名映射到键值类型

ts
type Stringify<T> = {
  [K in keyof T]: string;
};

假设传入的泛类是对象,使用keyof获取对象的键名联合类型,然后通过映射类型in将这个联合类型的每一个成员映射出来,并将其键值类型设为string

ts
type Stringify<T> = {
  [K in keyof T]: string;
};
interface Person {
  name: string
  age: number
  isMarried: boolean
}
const a: Stringify<Person> = {
  name: 'a',
  age: '12',
  isMarried: ''
}

也可以拿到每个键值的类型

ts
type Clone<T> = {
  [K in keyof T]: T[K];
};

typeof 类型查询操作符

typeof 返回的是一个 TypeScript 类型。

is 类型守卫

通过类型守卫函数来解决控制流分析的不足。使用关键字is

ts
function isString (input: unknown): input is string {
  return typeof input === 'string'
}
function foo (val: number | string) {
  if (isString(val)) val.concat('xxx')
}

也可以在类型守卫中使用对象类型、联合类型

ts
export type Falsy = false | '' | 0 | null | undefined;

export const isFalsy = (val: unknown): val is Falsy => !val

// 不包括不常用的 symbol 和 bigint
export type Primitive = string | number | boolean | undefined;

export const isPrimitive = (val: unknown): val is Primitive => ['string', 'number', 'boolean', 'undefined'].includes(typeof val)

asserts 类型断言守卫

和类型守卫最大的不同点在于,在判断条件不通过时,断言守卫需要抛出一个错误,类型守卫只需要剔除掉预期的类型。

ts
function assert (condition: any, msg?: string): asserts condition {
  if (!condition) throw new Error(msg)
}

const foo: any = 'yomuki'
assert(typeof foo === 'number')
foo.toFixed()

可以和 is 相结合

ts
function assertIsNumber (val: any): asserts val is number {
  if (!(typeof val === 'number')) throw new Error('Not a number!')
}

const foo: any = 'yomuki'
assertIsNumber(foo)
foo.toFixed()

模板字符串类型

类型中也可以使用模板字符串,但是只能使用stringnumberbooleannullundefinedbigint这几种类型。

ts
type Hello<T extends string | number | boolean | null | undefined | bigint> = `Hello ${T}`;

type Hello1 = Hello<'World'>; // "Hello World"
type Hello2 = Hello<null>; // "Hello null"
type Hello3 = Hello<undefined>; // "Hello undefined"

增强字符串字面量类型的灵活性,进一步增强类型和逻辑代码的关联:

ts
type Version = `${number}.${number}.${number}`;

const a: Version = '1.1.1'
const b: Version = 'a.b' // Type '"a.b"' is not assignable to type '`${number}.${number}.${number}`'.

当存在大量有关联的字面量时,可以使用模板字符串工具类型来提高代码的可读性:

ts
// type SKU =
//   | 'iphone-16G-official'
//   | 'xiaomi-16G-official'
//   | 'honor-16G-official'
//   | 'iphone-16G-second-hand'
//   | 'xiaomi-16G-second-hand'
//   | 'honor-16G-second-hand'
//   | 'iphone-64G-official'
//   | 'xiaomi-64G-official'
//   | 'honor-64G-official'
//   | 'iphone-64G-second-hand'
//   | 'xiaomi-64G-second-hand'
//   | 'honor-64G-second-hand';

type Brand = 'iphone' | 'xiaomi' | 'honor';
type Memory = '16G' | '64G';
type ItemType = 'official' | 'second-hand';

type SKU = `${Brand}-${Memory}-${ItemType}`;
// 可以利用差集来剔除不需要的字面量

与索引类型 和 映射类型 结合使用

ts
interface Foo {
  name: string
  age: number
}

interface ChangeListener {
  on: (change: `${keyof Foo}Changed`) => void
}

declare let listener: ChangeListener

// 提示并约束为 "nameChanged" | "ageChanged"
listener.on('')

结合重映射([K in keyof T asrename_${string & K}]),可以实现在映射键名时基于原键名做修改:

ts
interface Foo {
  name: string
  age: number
}

type CopyWithRename<T extends Object> = {
  [K in keyof T as `rename_${string & K}`]: T[K];
};

type Bar = CopyWithRename<Foo>;
// {
//     rename_name: string;
//     rename_age: number;
// }

因为键名合法值中包含了symbol,所以在使用模板字符串类型时,需要使用string & K来保证键名是字符串类型。

与匹配类型 结合使用

ts
type ReverseName<Str extends string> = Str extends `${infer First} ${infer Last}` ? `${Last} ${First}` : Str;
type ReverseNameTest = ReverseName<'Zeng Shuai'>; // Shuai Zeng