TS基础应用 & Hook中的TS

说在前面

   本文难度偏中下,涉及到的点大多为如何在项目中合理应用ts,小部分会涉及一些原理,受众面较广,有无TS基础均可放心食用。
                        **>>>> 阅完本文,您可能会收获到<<<<**
  1. 若您还不熟悉 TS,那本文可帮助您完成 TS 应用部分的学习,伴随众多 Demo 例来引导业务应用;
  2. 若您比较熟悉 TS,那本文可当作复习文,带您回顾知识,希望能在某些点引发您新发现和思考;
  3. 针对于 class 组件的 IState 和 IProps,类比 Hook 组件的部分写法和思考;

🌟🌟🌟TIPS:超好用的在线 TS 编辑器(诸多配置项可手动配置) 传送门:TS 在线 🌟🌟🌟

一、什么是 TS

不扯晦涩的概念,通俗来说 TypeScript 就是 JavaScript 的超集,它具有可选的类型,并可以编译为纯 JavaScript 运行。(笔者一直就把 TypeScript 看作 JavaScript 的 Lint)那么问题来了,为什么 TS 一定要设计成静态的? 或者换句话说,我们为什么需要向 JavaScript 添加类型规范呢 ?

经典自问自答环节——因为它可以解决一些 JS 尚未解决的痛点:

  1. JS 是动态类型的语言,这也意味着在实例化之前我们都不知道变量的类型,但是使用 TS 可以在运行前就避免经典低级错误。 例: Uncaught TypeError:'xxx' is not a function

⚠️ 典中典级别的错误🌰:

file
file
file

JS 就是这样,只有在运行时发生了错误才告诉我有错,但是当 TS 介入后:

file
file

好家伙!直接把问题在编辑器阶段抛出,nice!

  1. 懒人狂欢。 规范方便,又不容易出错,对于 VS Code,它能做的最多只是标示出有没有这个属性,但并不能精确的表明这个属性是什么类型,但 TS 可以通过类型推导/反推导(说白话:如果您未明确编写类型,则将使用类型推断来推断您正在使用的类型),从而完美优化了代码补全这一项:
file
file

第一个 Q&A——思考 :

那么我们还能想到在业务开发中 TS 解决了哪些 JS 的痛点呢?(提问)

回答,总结,补充:
-对函数参数的类型限制;
-对数组和对象的类型限制,避免定义出错 例如数据解构复杂或较多时,
可能会出现数组定义错误 a = { }, if (a.length){ // xxxxx }
-let functionA = 'jiawen' // 实际上 let functionA: string = 'jiawen'

  1. 使我们的应用代码更易阅读和维护,如果定义完善,可以通过类型大致明白参数的作用;

相信通过上述简单的bug-demo,各位已对TS有了一个初步的重新认识
接下来的章节便正式介绍我们在业务开发过程中如何用好TS

二、怎么用 TS

 在业务中如何用TS/如何用好TS?这个问题其实和 " 在业务中怎么用好一个API " 是一样的。首先要知道这个东西在干嘛,参数是什么,规则是什么,能够接受有哪些扩展......等等。 简而言之,撸它!
file

TS 常用类型归纳

通过对业务中常见的 TS 错误做出的一个综合性总结归纳,希望 Demos 会对您有收获

元语(primitives)之 string number boolean

  笔者把基本类型拆开的原因是: 不管是中文还是英文文档,primitives/元语/元组 这几个名词都频繁出镜,笔者理解的白话:希望在类型约束定义时,使用的是字面量而不是内置对象类型,官方文档:
file
let a: string = 'jiawen';
let flag: boolean = false;
let num: number = 150

interface IState: {
  flag: boolean;
  name: string;
  num: number;
}

元组

// 元组类型表示已知元素数量和类型的数组,各元素的类型不必相同,但是对应位置的类型需要相同。

let x: [string, number];
x = ['jiawen', 18];   // ok
x = [18, 'jiawen'];    // Erro
console.log(x[0]);    // jiawen

undefined null

let special: string = undefined
// 值得一提的是 undefined/null 是所有基本类型的子类,
// 所以它们可以任意赋值给其他已定义的类型,这也是为什么上述代码不报错的原因

object 和 { }

// object 表示的是常规的 Javascript对象类型,非基础数据类型
const offDuty = (value: object) => {
  console.log("value is ",  value);
}

offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // Error


//  {} 表示的是 非null / 非undefined 的任意类型
const offDuty = (value: {}) => {
  console.log("value is ", value);
}

offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // ok

//  {} 和Object几乎一致,区别是Object会对Object内置的 toString/hasOwnPreperty 进行校验
const offDuty = (value: Object) => {
  console.log("value is ",  value);
}

offDuty({ prop: 0}) // ok
offDuty(null) offDuty(undefined) // Error
offDuty(18) offDuty('offDuty') offDuty(false) // ok
offDuty({ toString(){ return 333 } }) // Error

如果需要一个对象类型,但对属性没有要求,建议使用 object 
{} 和 Object 表示的范围太大,建议尽量不要使用

object of params

// 我们通常在业务中可多采用点状对象函数(规定参数对象类型)

const offDuty = (value: { x: number; y: string }) => {
  console.log("x is ", value.x);
  console.log("y is ", value.y);
}

// 业务中一定会涉及到"可选属性";先简单介绍下方便快捷的“可选属性”

const offDuty = (value: { x: number; y?: string }) => {
  console.log("必选属性x ", value.x);
  console.log("可选属性y ", value.y);
  console.log("可选属性y的方法 ", value.y.toLocaleLowerCase());
}
offDuty({ x: 123, y: 'jiawen' })
offDuty({ x: 123 }) 

// 提问: 上述代码有问题吗?

答案:

// offDuty({ x: 123 }) 会导致结果报错value.y.toLocaleLowerCase()
// Cannot read property 'toLocaleLowerCase' of undefined

方案1: 手动类型检查
const offDuty = (value: { x: number; y?: string }) => {
    if (value.y !== undefined) {
            console.log("可能不存在的 ", value.y.toUpperCase());
  }
}
方案2:使用可选属性 (推荐)
const offDuty = (value: { x: number; y?: string }) => {
  console.log("可能不存在的 ", value.y?.toLocaleLowerCase());
}

unknown 与 any

// unknown 可以表示任意类型,但它同时也告诉TS, 开发者对类型也是无法确定,做任何操作时需要慎重

let Jiaven: unknown

Jiaven.toFixed(1) // Error

if (typeof Jiaven=== 'number') {
  Jiaven.toFixed(1) // OK
}

当我们使用any类型的时候,any会逃离类型检查,并且any类型的变量可以执行任意操作,编译时不会报错

anyscript === javascript

注意:any 会增加了运行时出错的风险,不到万不得已不要使用;

如果遇到想要表示【不知道什么类型】的场景,推荐优先考虑 unknown

union 联合类型

union也叫联合类型,由两个或多个其他类型组成,表示可能为任何一个的值,类型之间用 ' | '隔开

type dayOff = string | number | boolean

联合类型的隐式推导可能会导致错误,遇到相关问题请参考语雀 code and tips —— 《TS的隐式推导》

.值得注意的是,如果访问不共有的属性的时候,会报错,访问共有属性时不会.上个最直观的demo

function dayOff (value: string | number): number {
    return value.length;
}
// number并不具备length,会报错,解决方法:typeof value === 'string'

function dayOff (value: string | number): number {
    return value.toString();
}
// number和string都具备toString(),不会报错

never

// never是其它类型(包括 null 和 undefined)的子类型,代表从不会出现的值。

// 那never在实际开发中到底有什么作用? 这里笔者原汁原味照搬尤雨溪的经典解释来做第一个例子

第一个例子,当你有一个 union type:

interface Foo {
  type: 'foo'
}

interface Bar {
  type: 'bar'
}

type All = Foo | Bar

在 switch 当中判断 type,TS是可以收窄类型的 (discriminated union):

function handleValue(val: All) {
  switch (val.type) {
    case 'foo':
      // 这里 val 被收窄为 Foo
      break
    case 'bar':
      // val 在这里是 Bar
      break
    default:
      // val 在这里是 never
      const exhaustiveCheck: never = val
      break
  }
}

注意在 default 里面我们把被收窄为 never 的 val 赋值给一个显式声明为 never 的变量。
    
如果一切逻辑正确,那么这里应该能够编译通过。但是假如后来有一天你的同事改了 All 的类型:

    type All = Foo | Bar | Baz

然而他忘记了在 handleValue 里面加上针对 Baz 的处理逻辑,
这个时候在 default branch 里面 val 会被收窄为 Baz,导致无法赋值给 never,产生一个编译错误。
所以通过这个办法,你可以确保 handleValue 总是穷尽 (exhaust) 了所有 All 的可能类型。

第二个用法  返回值为 never 的函数可以是抛出异常的情况
function error(message: string): never {
    throw new Error(message);
}

第三个用法 返回值为 never 的函数可以是无法被执行到的终止点的情况
function loop(): never {
    while (true) {}
}

void

interface IProps {
  onOK: () => void
}
void 和 undefined 功能高度类似,但void表示对函数的返回值并不在意或该方法并无返回值

enum

笔者认为ts中的enum是一个很有趣的枚举类型,它的底层就是number的实现

1.普通枚举
enum Color {
  Red, 
  Green, 
  Blue
};
let c: Color = Color.Blue;
console.log(c); // 2

2.字符串枚举
enum Color {
  Red = 'red', 
  Green = 'not red', 
};

3.异构枚举 / 有时也叫混合枚举
enum Color {
  Red = 'red', 
  Num = 2, 
};
<第一个坑>

enum Color {
  A,         // 0
  B,         // 1
  C = 20,    // 20
  D,         // 21
  E = 100,   // 100
  F,         // 101
}

若初始化有部分赋值,那么后续成员的值为上一个成员的值加1
<第二个坑> 这个坑是第一个坑的延展,稍不仔细就会上当!

const getValue = () => {
  return 23
}

enum List {
  A = getValue(),
  B = 24,  // 此处必须要初始化值,不然编译不通过
  C
}
console.log(List.A) // 23
console.log(List.B) // 24
console.log(List.C) // 25

如果某个属性的值是计算出来的,那么它后面一位的成员必须要初始化值。
否则将会 Enum member must have initializer.

泛型

笔者理解的泛型很白话:先不指定具体类型,通过传入的参数类型来得到具体类型
我们从下述的 filter-demo 入手,探索一下为什么一定需要泛型

  • 泛型的基础样式
function fun<T>(args: T): T {
    return args
}

如果没接触过,是不是会觉得有点懵? 没关系!我们直接从业务角度深入——

1.刚开始的需求:过滤数字类型的数组

declare function filter(
    array: number[], 
  fn: (item: unknown) => boolean
) : number[];

2.产品改了需求:还要过滤一些字符串 string[] 

彳亍,那就利用函数的重载, 加一个声明, 虽然笨了点,但是很好理解

declare function filter(
  array: string[],
  fn: (item: unknown) => boolean
): string[];

declare function filter(
  array: number[],
  fn: (item: unknown) => boolean
): number[];

3.产品又来了! 这次还要过滤 boolean[]、object[] ..........

这个时候如果还是选择重载,将会大大提升工作量,代码也会变得越来越累赘,这个时候泛型就出场了,
它从实现上来说更像是一种方法,通过你的传参来定义类型,改造如下:

declare function filter<T>(
  array: T[],
  fn: (item: unknown) => boolean
): T[];

泛型中的<T>可以是任意,但是大部分偏好为 T、U、S 等,

当我们把泛型理解为一种方法实现后,那么我们便很自然的联想到:方法有多个参数、默认值,泛型也可以

type Foo<T, U = string> = { // 多参数、默认值
  foo: Array<T> // 可以传递
  bar: U
}

type A = Foo<number> // type A = { foo: number[]; bar: string; }
type B = Foo<number, number> // type B = { foo: number[]; bar: number; }

既然是“函数”,那也会有“限制”,下文列举一些稍微常见的约束

1. extends: 限制 T 必须至少是一个 XXX 的类型

type dayOff<T extends HTMLElement = HTMLElement> = {
   where: T,
   name: string
}
2. Readonly<T>: 构造一个所有属性为readonly,这意味着无法重新分配所构造类型的属性。

interface Eat {
  food: string;
}

const todo: Readonly<Eat> = {
  food: "meat beef milk",
};

todo.food = "no food"; // Cannot assign to 'title' because it is a read-only property.
3. Pick<T,K>: 从T中挑选出一些K属性

interface Todo {
  name: string;
  job: string;
  work: boolean;


type TodoPreview = Pick<Todo, "name" | "work">;

const todo: TodoPreview = {
  name: "jiawen",
  work: true,
};
todo;
4. Omit<T, K>: 结合了 T 和 K 并忽略对象类型中 K 来构造类型。

interface Todo {
  name: string;
  job: string;
  work: boolean;
}

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

const todo: TodoPreview = {
  name: "jiawen",
  job: 'job',
};

5.Record: 约束 定义键类型为 Keys、值类型为 Values 的对象类型。

enum Num {
  A = 10001,
  B = 10002,
  C = 10003
}

const NumMap: Record<Num, string> = { 
  [Num.A]: 'this is A',
  [Num.B]: 'this is B'
}
// 类型 "{ 10001: string; 10002: string; }" 中缺少属性 "10003",
// 但类型 "Record<ErrorCodes, string>" 中需要该属性,所以我们还可以通过Record来做全面性检查

keyof 关键字可以用来获取一个对象类型的所有 key 类型
type User = {
  id: string;
  name: string;
};

type UserKeys = keyof User;  // "id" | "name"

改造如下

type Record<K extends keyof any, T> = {
  [P in K]: T;
};
此时的 T 为 any;
还有一些不常用,但是很易懂的:

6. Extract<T, U>  从T,U中提取相同的类型

7. Partial<T>    所有属性可选

type User = {
  id?: string,
  gender: 'male' | 'female'
}

type PartialUser =  Partial<User>  // { id?: string, gender?: 'male' | 'female'}
  
type Partial<T> = { [U in keyof T]?: T[U] }

8. Required<T>   所有属性必须 << === >> 与Partial相反

type User = {
  id?: string,
  sex: 'male' | 'female'
}

type RequiredUser = Required<User> // { readonly id: string, readonly gender: 'male' | 'female'}

function showUserProfile (user: RequiredUser) {
  console.log(user.id) // 这时候就不需要再加?了
  console.log(user.sex)
}
type Required<T> = { [U in keyof T]-?: T[U] };   -? : 代表去掉?



三、TS 的一些须知

TS 的 type 和 interface

  • interface(接口) 只能声明对象类型,支持声明合并(可扩展)。
interface User {
  id: string
}
 
interface User {
  name: string
}
 
const user = {} as User
 
console.log(user.id);
console.log(user.name);

  • type(类型别名)不支持声明合并 -- l 类型
type User = {
  id: string,
}

if (true) {
  type User = {
    name: string,
  }

  const user = {} as User;
  console.log(user.name);
  console.log(user.id) // 类型“User”上不存在属性“id”。
}

file

🌟🌟🌟🌟🌟🌟
type 和 interface 异同点总结:

  1. 通常来讲 type 更为通用,右侧可以是任意类型,包括表达式运算,以及映射等;
  2. 凡是可用 interface 来定义的,type 也可;
  3. 扩展方式也不同,interface 可以用 extends 关键字进行扩展,或用来 implements 实现某个接口;
  4. 都可以用来描述一个对象或者函数;
  5. type 可以声明基本类型别名、联合类型、元组类型,interface 不行;
  6. ⚠️ 但如果你是在开发一个包,模块,允许别人进行扩展就用 interface,如果需要定义基础数据类型或者需要类型运算,使用 type。
  7. interface 可以被多次定义,并会被视作合并声明,而 type 不支持;
  8. 导出方式不同,interface 支持同时声明并默认导出,而 typetype 必须先声明后导出;

TS 的脚本模式和模块模式

Typescript 存在两种模式,区分的逻辑是,文件内容包不包含 import 或者 export 关键字

脚本模式(Script) 一个文件对应一个 html 的 script 标签,
模块模式(Module)一个文件对应一个 Typescript 的模块。

脚本模式下,所有变量定义,类型声明都是全局的,多个文件定义同一个变量会报错,同名 interface 会进行合并;而模块模式下,所有变量定义,类型声明都是模块内有效的。

两种模式在编写类型声明时也有区别,例如脚本模式下直接 declare var GlobalStore 即可为全局对象编写声明。

例子:

  • 脚本模式下直接 declare var GlobalStore 即可为全局对象编写声明。
GlobalStore.foo = "foo";
GlobalStore.bar = "bar"; // Error

declare var GlobalStore: {
  foo: string;
};
  • 模块模式下,要为全局对象编写声明需要 declare global
GlobalStore.foo = "foo";
GlobalStore.bar = "bar";

declare global {
  var GlobalStore: {
    foo: string;
    bar: string;
  };
}

export {}; // export 关键字改变文件的模式

TS 的索引签名

  • 索引签名可以用来定义对象内的属性、值的类型,例如定义一个 React 组件,允许 Props 可以传任意 key 为 string,value 为 number 的 props
interface Props {
  [key: string]: number
}

<Component count={1} /> // OK
<Component count={true} /> // Error
<Component count={'1'} /> // Error

TS 的类型键入

  • Typescript 允许像对象取属性值一样使用类型
type User = {
  userId: string
  friendList: {
    fristName: string
    lastName: string
  }[]
}

type UserIdType = User['userId'] // string
type FriendList = User['friendList'] // { fristName: string; lastName: string; }[]
type Friend = FriendList[number] // { fristName: string; lastName: string; }
  • 在上面的例子中,我们利用类型键入的功能从 User 类型中计算出了其他的几种类型。FriendList[number]这里的 number 是关键字,用来取数组子项的类型。在元组中也可以使用字面量数字得到数组元素的类型。
type group = [number, string]
type First =  group[0] // number
type Second = group[1] // string

TS 的断言

  • 类型断言不是类型转换,断言成一个联合类型中不存在的类型是不允许的
function getLength(value: string | number): number {
    if (value.length) {
        return value.length;
    } else {
        return value.toString().length;
    }
  
    // 这个问题在object of parmas已经提及,不再赘述
  
    修改后:
    
    if ((<string>value).length) {
        return (<string>value).length;
    } else {
        return something.toString().length;
    }
}

断言的两种写法

1. <类型>值:  <string>value

2. 或者 value as string

特别注意!!! 断言成一个联合类型中不存在的类型是不允许的

function toBoolean(something: string | number): boolean {
    return <boolean>something;
}
  • 非空断言符 !

TypeScript 还具有一种特殊的语法,用于从类型中删除 null 和 undefined 不进行任何显式检查。!在任何表达式之后写入实际上是一个类型断言,表明该值不是 null 或 undefined

function liveDangerously(x?: number | undefined | null) {
  // 推荐写法
  console.log(x!.toFixed());
}

四、如何在 Hook 组件中使用 TS

usestate

  • useState 如果初始值不是 null/undefined 的话,是具备类型推导能力的,根据传入的初始值推断出类型;初始值是 null/undefined 的话则需要传递类型定义才能进行约束。一般情况下,还是推荐传入类型(通过 useState 的第一个泛型参数)。
// 这里ts可以推断 value的类型并且能对setValue函数调用进行约束
const [value, setValue] = useState(0);

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

// 这里需要传递MyObject才能约束 value, setValue
// 所以我们一般情况下推荐传入类型
const [value, setValue] = useState<MyObject>(null);

-----as unkonwn as unkownun

useEffect useLayoutEffect

  • 没有返回值,无需类型传递和约束

useMemo useCallback

  • useMemo 无需传递类型, 根据函数的返回值就能推断出类型。
  • useCallback 无需传递类型,根据函数的返回值就能推断出类型。

但是注意函数的入参需要定义类型,不然将会推断为 any!

const value = 10;

const result = useMemo(() => value * 2, [value]); // 推断出result是number类型

const multiplier = 2;
// 推断出 (value: number) => number
// 注意函数入参value需要定义类型
const multiply = useCallback((value: number) => value * multiplier, [multiplier]);

useRef

  • useRef 传非空初始值的时候可以推断类型,同样也可以通过传入第一个泛型参数来定义类型,约束 ref.current 的类型。
1. 如果传值为null
const MyInput = () => {
  const inputRef = useRef<HTMLInputElement>(null); // 这里约束inputRef是一个html元素
  return <input ref={inputRef} />
}
  
2. 如果不为null
const myNumberRef = useRef(0);  // 自动推断出 myNumberRef.current 是number类型
myNumberRef.current += 1;

useContext

  • useContext 一般根据传入的 Context 的值就可以推断出返回值。一般无需显示传递类型
type Theme = 'light' | 'dark';
// 我们在createContext就传了类型了
const ThemeContext = createContext<Theme>('dark');

const App = () => (
  <ThemeContext.Provider value="dark">
    <MyComponent />
  </ThemeContext.Provider>
)

const MyComponent = () => {
    // useContext根据ThemeContext推断出类型,这里不需要显示传
  const theme = useContext(ThemeContext);
  return <div>The theme is {theme}</div>;

五、关于 TS 的一些思考

1. 关于 TSC 如何把 TS 代码转换为 JS 代码

这个部分比较冗长,后续可以单独出一篇文章(2)来专门探索。
  • 不过,tsconfig.json 的部分常用的配置属性表还是值得一提的
{
  "compilerOptions": {
    "noEmit": true, // 不输出文件
    "allowUnreachableCode": true, // 不报告执行不到的代码错误。
    "allowUnusedLabels": false, // 不报告未使用的标签错误
    "alwaysStrict": false, // 以严格模式解析并为每个源文件生成 "use strict"语句
    "baseUrl": ".", // 工作根目录
    "lib": [ // 编译过程中需要引入的库文件的列表
      "es5",
      "es2015",
      "es2016",
      "es2017",
      "es2018",
      "dom"
    ]
    "experimentalDecorators": true, // 启用实验性的ES装饰器
    "jsx": "react", // 在 .tsx文件里支持JSX
    "sourceMap": true, // 是否生成map文件
    "module": "commonjs", // 指定生成哪个模块系统代码
    "noImplicitAny": false, // 是否默认禁用 any
    "removeComments": true, // 是否移除注释
    "types": [ //指定引入的类型声明文件,默认是自动引入所有声明文件,一旦指定该选项,则会禁用自动引入,改为只引入指定的类型声明文件,如果指定空数组[]则不引用任何文件
      "node", // 引入 node 的类型声明
    ],
    "paths": { // 指定模块的路径,和baseUrl有关联,和webpack中resolve.alias配置一样
      "src": [ //指定后可以在文件之直接 import * from 'src';
        "./src"
      ],
    },
    "target": "ESNext", // 编译的目标是什么版本的
    "outDir": "./dist", // 输出目录
    "declaration": true, // 是否自动创建类型声明文件
    "declarationDir": "./lib", // 类型声明文件的输出目录
    "allowJs": true, // 允许编译javascript文件。
  },
  // 指定一个匹配列表(属于自动指定该路径下的所有ts相关文件)
  "include": [
    "src/**/*"
  ],
  // 指定一个排除列表(include的反向操作)
  "exclude": [
    "demo.ts"
  ],
  // 指定哪些文件使用该配置(属于手动一个个指定文件)
  "files": [
    "demo.ts"
  ]
}

2. TS 泛型的底层实现

关于TS泛型进阶篇 链接:[https://dtstack.yuque.com/rd-center/sm6war/wae3kg](https://dtstack.yuque.com/rd-center/sm6war/wae3kg)


这个部分比较复杂,笔者还需沉淀,欢迎各位直接留言或在文章中补充!!!

3. TS 泛型+类型反推在实际开发中的应用

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

推荐阅读更多精彩内容

  • ts的优点:静态编译提示错误,强类型,接口和继承,是js的超集,需要编译后执行 编译 1、ts后缀建立文件后通过t...
    看到这朵小fa了么阅读 452评论 0 0
  • 背景介绍 JavaScript创立20多年,已经从当初只是为网页添加琐碎交互的小型脚本语言发展成应用最广泛的跨平台...
    Eason_0cce阅读 11,014评论 0 7
  • TypeScript是微软开发的,基于类的面向对象编程,其文件以 .ts 为后缀名; TypeScript是Jav...
    hellomyshadow阅读 953评论 0 0
  • Vue3 基本全部使用 TypeScript 来进行重写,尽管你可能觉得要学的东西越来越多了,但是作为程序员,如果...
    橙色流年阅读 615评论 0 1
  • 一、TypeScript 概述 TypeScript 是微软开发的自由和开源的编程语言; 是 JavaScript...
    yjtuuige阅读 979评论 0 8