2023-07-27

TypeScript 玩转类型操作之基础篇(回来,不是讲基础类型)

引言

本文的目的是探索 TypeScript 类型操作的基础知识,先介绍 typeof 、keyof、extends、infer 等操作关键词的使用方式(作为基础);然后介绍TS提供的通用类型工具(详细介绍官方源码具体实现);这两个部分熟练掌握,我们就掌握TS体操的核心内容了,剩下的就是考虑根据类型目标情况如何选择类型操作顺序。欢迎成为体操小能手~

typeof

typeof 在 JS 中用于获取一个变量的基础数据类型,在 TS 中 typeof 用于获取运行时变量或者属性的类型。在TS代码中需要注意这两种用法之间的差异,使用的情况不同的语义环境下有不能的表现形式。

typeof 以JS的判断变量类型时工作时,返回值是一个字符串内容是其中之一("string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function")。

const s = "hello";
let n = typeof s; // n 值是 "string",n 类型是 let n: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"

let m: typeof s; // let m: stringconst s = "hello";
let n = typeof s; // n 值是 "string",n 类型是 let n: "string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"

let m: typeof s; // let m: string

typeof 作为TS访问运行时值读取类型时,这个类型经过读取 type context 进行类型推导得出【返回的是一个TS的类型值,内容来自:自定义的类型修饰、无类型修饰时默认推导类型、强制类型转换后的类型结果】。

const point = { x:1,y:1 };

// 没有指定类型,TS 默认类型推理推导出合适类型
type PointType = typeof point;

/* 等价于
type PointType = {
    x: number;
    y: number;
}
*/


const person = { name: 'xxx'  } as any;

// as 强制类型转换为 any, 使用强制类型转化的类型作为结果
type PersonType = typeof person;
// 等价于 type PersonType = any


type Car = {
    drive: ()=>void;
    name?: string;
}
const car: Car = { drive:()=>{} }
// 使用了指定的类型作为修饰,结果返回类型为指定的类型
type CarType = typeof car;

/* 等价于
type CarType = {
    drive: () => void;
    name?: string | undefined;
}
*/const point = { x:1,y:1 };

// 没有指定类型,TS 默认类型推理推导出合适类型
type PointType = typeof point;

/* 等价于
type PointType = {
    x: number;
    y: number;
}
*/


const person = { name: 'xxx'  } as any;

// as 强制类型转换为 any, 使用强制类型转化的类型作为结果
type PersonType = typeof person;
// 等价于 type PersonType = any


type Car = {
    drive: ()=>void;
    name?: string;
}
const car: Car = { drive:()=>{} }
// 使用了指定的类型作为修饰,结果返回类型为指定的类型
type CarType = typeof car;

/* 等价于
type CarType = {
    drive: () => void;
    name?: string | undefined;
}
*/

typeof 的作用就是从JavaScript运行时变量值上提取出对应的TS类型,在 TS 体操中它是基础的类型操作。和 keyof 、 Omit、Partial、Record 等搭配起来非常好用。常用于:类型收窄(读取运行时特定字段的类型,用于判断)做类型守卫。

keyof

keyof 用于提取一个对象类型的属性名,返回一个字符串或数字字面量的联合类型。只有一个属性时返回字符串,多个属性时返回联合类型 ('xx' | 'yy'),映射类型或条件类型下返回值会携带特殊的类型联合值。

普通对象类型 keyof 提取属性,interface 也能正常提取。注意不要对普通类型【string、number、联合类型等】使用 keyof 。

const point = { x:1,y:1 };

type PointType = typeof point;

const person = { name: 'xxx'  } as any;

type PersonType = typeof person;

type PointKeysType = keyof PointType;
// 等价于 type PointKeysType = "x" | "y"
const point = { x:1,y:1 };

type PointType = typeof point;

const person = { name: 'xxx'  } as any;

type PersonType = typeof person;

type PointKeysType = keyof PointType;
// 等价于 type PointKeysType = "x" | "y"

映射类型使用 keyof 时提取不到属性,此时默认返回可以作为对象属性值的类型联合string | number | symbol。映射类型为每个属性指定同样的类型,一般用于同一类型的注册表对象类型定义。

type ToolsType = {
    [k:string]: Function
}

type ToolsKeys = keyof ToolsType; // string | number

// 将一个类型T上的所有属性值作为新类型的 key 构建新类型
type OptionsFlags<T> = {
    [k in keyof T]: boolean
}

type CarOpFlag = keyof OptionsFlags // 这里写法不好,只做个演示。 等价于:type CarOpFlag = string | number | symbol
type ToolsType = {
    [k:string]: Function
}

type ToolsKeys = keyof ToolsType; // string | number

// 将一个类型T上的所有属性值作为新类型的 key 构建新类型
type OptionsFlags<T> = {
    [k in keyof T]: boolean
}

type CarOpFlag = keyof OptionsFlags // 这里写法不好,只做个演示。 等价于:type CarOpFlag = string | number | symbol

keyof 的原理是基于索引类型查询(Index Type Query)和索引访问操作符(Index Access Operator)的组合。当我们使用 keyof 时,它会对指定的类型进行索引类型查询,返回该类型的所有键组成的联合类型。

extends

extends 关键词用于类型之间的扩展和约束,主要有以下功能。

一、用于扩展一个类型或接口,使其继承另一个类型或接口的成员。通常用于接口继承接口,类继承父类

interface User {
    name: string;
}

interface Student extends User {
    age: number;
}

const s:Student = {
    age: 11,
    name: 'xx'
};
interface User {
    name: string;
}

interface Student extends User {
    age: number;
}

const s:Student = {
    age: 11,
    name: 'xx'
};

Student 使用 extends 继承 User 接口的 name 属性并新增了新的 age 属性。

二、泛型约束,用于约束泛型的类型范围,指定泛型参数必须满足某些条件。

interface User {
    name: string;
}

interface Student extends User {
    age: number;
}

function registeredStudent<T extends User>(user: T) {
    // ...
}
interface User {
    name: string;
}

interface Student extends User {
    age: number;
}

function registeredStudent<T extends User>(user: T) {
    // ...
}

限制 T 的类型最少要满足 User 类型的条件也就是必须有 name 属性并且值是 string 。TS 的类型校验属于是鸭式变形【长得像鸭子,叫起来也像鸭子,那就是鸭子,才不管是不是老鼠脖子】,而不是 Java 这类强类型语言的判等。

三、类型条件,在类型条件中用于根据类型关系推导条件类型的结果

type GetReturnType<T> = T extends (...args: any[]) => infer Return ? Return : never;

type HandleType = (msg: string) => number | string;

type returnType = GetReturnType<HandleType>
// 等价于 type returnType = string | numbertype GetReturnType<T> = T extends (...args: any[]) => infer Return ? Return : never;

type HandleType = (msg: string) => number | string;

type returnType = GetReturnType<HandleType>
// 等价于 type returnType = string | number

这里的 extends 是三目运算符的判断条件,表示:如果T类型符合 (...args: any[]) => any 这个类型那么条件成立返回T类型的返回值类型,否则返回 never。infer 关键词用于在条件类型中提取部分类型保存到对应变量,infer详细请往下看。

infer

infer 关键字用于条件类型中的类型推断,它的实现原理是基于TS 条件类型和推断机制。功能是允许我们在条件类型中声明一个类型变量,用于在条件类型的真分支中推断出一个具体的类型。编译器会根据已知的类型信息进行类型推断,将推断出的类型赋值给 infer 声明的类型变量。对于TS体操来说,这个是基础。以下是 TypeScript 官方给我们提供的通用类型工具,基于 infer 和 extends 条件类型实现非常简单。

// TypeScript 官方提供的一些类型操作工具源码

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;
// TypeScript 官方提供的一些类型操作工具源码

/**
 * Obtain the parameters of a function type in a tuple
 */
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

/**
 * Obtain the return type of a constructor function type
 */
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : any;

TypeScript 提供处理类型的工具类型

随着 TS 各个版本不断补充,截止现在支持 22 种工具类型用于处理类型转换。由于比较多,我们将按照分类进行介绍。常说的TS体操实际就是掌握了这些工具类型,甚至我们可以自己定义一些通用的类型工具,然后按照要求进行类型之间的转换比如:将下划线命名转换成小驼峰命名、递归的提取指定类型的key等等。

以下是TypeScript 官方提供的类型工具支持。

Awaited<T> 提取异步 Promise 链上的返回值类型

用法

interface User {
    name: string;
}
type RequestUser = (id: number) => Promise<User>;

// 从返回值类型中提取到,Promise 里面的具体类型
type ResultType = Awaited<ReturnType<RequestUser>>interface User {
    name: string;
}
type RequestUser = (id: number) => Promise<User>;

// 从返回值类型中提取到,Promise 里面的具体类型
type ResultType = Awaited<ReturnType<RequestUser>>

源码实现,递归的从 Promise 链上读取返回值类型并进行条件推导,实现中的关键点就是条件判断逻辑,并使用 infer 提取到需要的类型进行返回。

/**
 * Recursively unwraps the "awaited type" of a type. Non-promise "thenables" should resolve to `never`. This emulates the behavior of `await`.
 */
type Awaited<T> =
    T extends null | undefined ? T : // special case for `null | undefined` when not in `--strictNullChecks` mode
        T extends object & { then(onfulfilled: infer F, ...args: infer _): any } ? // `await` only unwraps object types with a callable `then`. Non-object types are not unwrapped
            F extends ((value: infer V, ...args: infer _) => any) ? // if the argument to `then` is callable, extracts the first argument
                Awaited<V> : // recursively unwrap the value
                never : // the argument to `then` was not callable
        T; // non-object or non-thenable

Partial<T> 、Required<T>、Readonly<T>

这三兄弟并不会导致T类型上的属性数量变化,放在一起看。Partial<T> 将T类型中的所有属性转换成可选;Required<T> 则相反将T中可选属性转换成必要属性。Readonly<T> 将T上所有的属性都变成只可读限制。

用法

type DataType = {
  id: number;
  origin?: string;
}

function mergeData(data: DataType, rest: Partial<DataType>): DataType {
  return { ...data, ...rest };
}

function save(data: Required<DataType>) {
    // 写库需要完整的数据, 包括 id 和 origin 来个字段
}

const data: DataType = { id: 11 };

// ✅
const result = mergeData(data, {origin: 'DB-1'});

// ❌
save(data);
/* Error
Argument of type 'DataType' is not assignable to parameter of type 'Required<DataType>'.
  Types of property 'origin' are incompatible.
    Type 'string | undefined' is not assignable to type 'string'.
      Type 'undefined' is not assignable to type 'string'
*/

源码实现以及实现原理,使用 keyof 读取T类型中的所有属性,然后从T中读取对应的类型进行重新赋值。需要注意的是,此处不支持深层的处理,只处理了T的一级属性。我们稍后自定义封装一个能深度处理的工具类型。

// 源码实现
/**
 * Make all properties in T optional
 */
type Partial<T> = {
    [P in keyof T]?: T[P];
};

/**
 * Make all properties in T required
 */
type Required<T> = {
    [P in keyof T]-?: T[P];
};

/**
 * Make all properties in T readonly
 */
type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

ReturnType<T>、Parameters<T>、ConstructorParameters<T>

这三兄弟都是对函数进行处理的,放在一起。ReturnType<T> 如果T是一个函数类型那么返回T的返回值类型,否则返回 never;Parameters<T> 如果T是一个函数类型,读取其参数类型返回,非函数返回never,函参是个类数组实际返回的一个 tuple 类型;ConstructorParameters<T> 读取类上的构造函数的函参列表类型,返回值类型和 Parameters<T> 一致。

function dataTransfer(a: number, b: string, c: Array<number>): boolean {
    return false;
}

type dataTransferReturnType = ReturnType<typeof dataTransfer> 
// 值类型 type dataTransferReturnType = boolean

type dataTransferParametersType = Parameters<typeof dataTransfer> 
// 类型值 type dataTransferParametersType = [a: number, b: string, c: number[]]

interface User {
    name: string;
}

class Student {
    private name:string;
    private age:number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
}

type StudentConstructorParametersType = ConstructorParameters<typeof Student>
// 值类型: type StudentConstructorParametersType = [name: string, age: number]function dataTransfer(a: number, b: string, c: Array<number>): boolean {
    return false;
}

type dataTransferReturnType = ReturnType<typeof dataTransfer> 
// 值类型 type dataTransferReturnType = boolean

type dataTransferParametersType = Parameters<typeof dataTransfer> 
// 类型值 type dataTransferParametersType = [a: number, b: string, c: number[]]

需要注意的是,这里使用了 typeof 获取运行时的变量值类型,也就是: typeof Student 提取了 Student 类的类型;使用 typeof dataTransfer 来提取了dataTransfer 函数的类型。typeof 忘了的往前翻翻,已备下了。

实现原理,借助条件类型以及 infer 的能力,提取参数类型、提取返回值类型。很简单,看源码吧。

// 源码

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never;

/**
 * Obtain the parameters of a constructor function type in a tuple
 */
type ConstructorParameters<T extends abstract new (...args: any) => any> = T extends abstract new (...args: infer P) => any ? P : never;

/**
 * Obtain the return type of a function type
 */
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;

Pick<T, Keys>、Record<Keys, T>

Pick 和 Record 是两兄弟,大家可能有点疑问为什么不把Pick和Omit放在一起说【功能上Pick是得到交集,而Omit是选择补集】,不放在一起说是因为实现方式上 Omit 和 Exclude 更近一点。Pick<T, Keys> 表示从T中摘取属性名属于Keys中的类型集合; Record<Keys, T> 表示构造一个新的类型,属性名称遍历Keys,以T作为属性类型。

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

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

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

//##__________________________________##

interface CatInfo {
  age: number;
  breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

需要注意的是,Pick<T,Keys> 以及 Record<Keys, T> 中的 keys 是联合类型

实现原理,读取指定的Keys ,Pick从T中读取对应的类型,Record则完全使用传入的T作为属性类型

// 源码
/**
 * From T, pick a set of properties whose keys are in the union K
 */
type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends keyof any, T> = {
    [P in K]: T;
};

Exclude<UnionType, ExcludedMembers>、Extract<Type, Union>

对立的两兄弟,功能如下。

Exclude<UnionType, ExcludedMembers> 表示从UnionType 中排除可分配给 ExcludedMembers 里面的成员,返回剩下的类型。

Extract<Type, Union> 表示从Type中提取可以分配给Union并集的所有并集成员构造一个新的类型。

Exclude<UnionType, ExcludedMembers> 用法

type T0 = Exclude<"a" | "b" | "c", "a">;    
//type T0 = "b" | "c"

type T1 = Exclude<"a" | "b" | "c", "a" | "b">;   
//type T1 = "c"


type T2 = Exclude<string | number | (() => void), Function>;     
// type T2 = string | number
 
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };
 
type T3 = Exclude<Shape, { kind: "circle" }>
// type T3 =  | { kind: "square"; x: number } | { kind: "triangle"; x: number; y: number };

Extract<Type, Union> 用法

type T0 = Extract<"a" | "b" | "c", "a" | "f">;
//    ^?
// type T0 = "a";

type T1 = Extract<string | number | (() => void), Function>;
//    ^?
// T1 = ()=>void

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T2 = Extract<Shape, { kind: "circle" }>
//    ^?
// type T2 = { kind: "circle"; radius: number }
type T0 = Extract<"a" | "b" | "c", "a" | "f">;
//    ^?
// type T0 = "a";

type T1 = Extract<string | number | (() => void), Function>;
//    ^?
// T1 = ()=>void

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

type T2 = Extract<Shape, { kind: "circle" }>
//    ^?
// type T2 = { kind: "circle"; radius: number }

实现原理,源码库里面看到的类型定义看起来比较简单。

// 源码
/**
 * Exclude from T those types that are assignable to U
 */
type Exclude<T, U> = T extends U ? never : T;

/**
 * Extract from T those types that are assignable to U
 */
type Extract<T, U> = T extends U ? T : never;

可能大家会和我一样一头雾水,这一句怎么实现了联合类型与联合类型之间的计算呢?怎么也得有个遍历的过程吧?(简单说一下,TS 会针对联合类型执行对应的遍历过程,并且执行=后面的条件类型判断或操作,返回值会集中处理后返回给接受的类型变量(返回值可能是个联合类型、也可能是一个普通类型,根据自定义工具代码))。

我们简单实现一个提取联合类型中的指定属性对应的类型集合。

type Shape = 
  |{ kind: "circle"; radius: number, x:string }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: boolean; y: number };

// 自定义一个工具类型
type PickOneKeyType<T, K extends string> = K extends keyof T ? ({ [P in K]: T[P] })[K] : never

type XType = PickOneKeyType<Shape, 'x'>
// type XType = string | number | boolean
type KindType = PickOneKeyType<Shape, 'kind'>
// type kindType = "circle" | "square" | "triangle"

【答案请大家关注下一篇文章:《TypeScript 玩转类型下篇》】,我尽量研究清楚执行过程及流程。

Omit<T, Keys>

Omit<T, Keys> 用于构造一个新的类型属性和属性类型从T中读取,删除 Keys 中存在的属性。有点像 Exclude 。Exclude 处理联合类型,Omit 处理对象类型,属性级别

用法

type PointType = {
  x: number;
  y: number;
  getCoords: ()=> [number, number]
}

type DataType = Omit<PointType, 'getCoords'>
/*
type DataType = {
    x: number;
    y: number;
}

Omit<T, K>实现源码,先将筛选过滤得到目标属性集合,然后从T中读取该属性集合的类型。 使用到 Pick 和 Exclude 两个类型工具。

// 源码
/**
 * Construct a type with the properties of T except for those in type K.
 */
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;

总结

本文属于 《TypeScript 玩转类型操作》系列第一篇,可能大家好奇为什么没有字符串方向的介绍?没有自定义类型工具实战?没有TS体操面试题?

原因是TS对模板字符串的功能很强大内容很多,所以将模板字符串+字符串处理工具类型放到一篇文章中,敬请关注。

类型工具实战,也会单开一篇文章也来写。包含:深度 Partial、深度 Required 以及其他常用的类型工具定义。

TS体操面试题,还在收集中,好久没面试了,找到的题目不多,看到这儿的小伙伴欢迎评论区帮忙出出题。

留下不少的TODO ……

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,056评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,842评论 2 378
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,938评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,296评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,292评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,413评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,824评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,493评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,686评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,502评论 2 318
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,553评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,281评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,820评论 3 305
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,873评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,109评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,699评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,257评论 2 341

推荐阅读更多精彩内容