TypeScript 基础入门

237 阅读7分钟

TypeScript 基础入门

类型定义

基础类型请用小写定义类型 例如 string number,因为 String Number 是 JS 对象

ts 具有自动类型推断 大部分时候不需要定义类型,或者显式的定义变量的类型注释

string

let str1 = 'string'
const str2: string = 'string'
str1 = 1 // Type 'number' is not assignable to type 'string'.

number

let num1 = 3.14
const num2: number = 3.14
num1 = false // Type 'boolean' is not assignable to type 'number'.

boolean

let bool1 = true
const bool2: boolean = true
bool1 = null // Type 'null' is not assignable to type 'boolean'.

null

let null1 = null // 如果赋值 null 或者 undefined 且没有定义类型 ts 会推断成 any 类型
const null2: null = null
null1 = 2 // any 可以赋值任何类型
null1 = '' // any 可以赋值任何类型

undefined

let void1 = undefined // 如果赋值 null 或者 undefined 且没有定义类型 ts 会推断成 any 类型
const void2: undefined = undefined
void1 = 2 // any 可以赋值任何类型
void1 = '' // any 可以赋值任何类型

symbol

let sym1 = Symbol.iterator
const sym2: symbol = Symbol('sym2')
sym1 = 1 // Type 'number' is not assignable to type 'symbol'.

bigint

let bigint1 = 1n
const bigint2: bigint = BigInt(9007199254740991)
bigint1 = 1 // Type 'number' is not assignable to type 'bigint'.

any

// 类似 dart 中的 dynamic 类型
// any 必须要定义类型,不然 ts 会尝试推断成其他类型
// 可以被赋值为任意类型 使用时 ts 不会强制对类型进行校验
let any1: any = {a: 1, b: '2'}
any1 = 1 // 如果上面没有定义 any 类型,会提示 Type 'number' is not assignable to type '{ a: number; b: string; }'.
any1 = true // 如果上面没有定义 any 类型,会提示 Type 'boolean' is not assignable to type '{ a: number; b: string; }'

unknown

// 类似 dart 中的 Object 类型
// 可以被赋值为任意类型 使用时 ts 会强制对类型进行校验
function invokeUnknownCallback(callback: unknown): void {
    // callback() // 'callback' is of type 'unknown'.
    if (typeof callback === 'function') callback()
}

function invokeAnyCallback(callback: any): void {
    callback()
}

array

// 除了基础类型,其他类型定义数组时推荐添加上泛型
const arr1: any[] = [1, '2', true]
const arr2: string[] = ['1', '2']
const arr3: Array<string> = ['1', '2']
const arr4 = [1, 2] // 会自动推断类型为 number[]
arr1.push(null) // success
arr2.push(3) // Argument of type 'number' is not assignable to parameter of type 'string'.
arr3.push(3) // Argument of type 'number' is not assignable to parameter of type 'string'.

tuple

返回固定长度和类型的数组 React.useState

// 以下是很经典的元组类型使用场景 [React.useState](https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/react/index.d.ts#L941C5-L941C97) 类型定义的一种
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>]

const [getVal, setVal] = useState<string>('value')
// useState[0] 表示 get
// useState[1] 表示 set

function

const fn1 = (age: number): number => age * 2

function fn2(sex: 0 | 1): string {
    return ['女', '男'][sex]
}

void

表示不返回值的函数 的返回值

function fn1(): void {
    // do something
    // 没有返回值
}

function fn2(): number {
    // do something
    // 有返回值
    return 1
}

never

永远都不会有返回值

// 下面的函数抛出了异常,永远不会有返回值
function neverReturns(): never {
    throw new TypeError('never 类型永远不会返回值')
}

// 死循环 也不会有返回值
while (true) {}

自定义类型

enum

枚举

enum SexType {
    Female = 0,
    Male // 自动推断类型1
}

// SexType 编译后输出
var SexType = /*#__PURE__*/function (SexType) {
    SexType[SexType["Female"] = 0] = "Female";
    SexType[SexType["Male"] = 1] = "Male";
    return SexType;
}(SexType || {});

// 如果枚举需要 toString 请在就近定义
function SexTypeToString(sex: SexType): string {
    switch (sex) {
        case SexType.Male:
            return '男'
        case SexType.Female:
            return '女'
    }
}

enum UserType {
    Admin = 2, // 默认从0开始自增
    Teacher, // 3
    Developer = 'Developer'
}

// UserType 编译后输出
var UserType = /*#__PURE__*/function (UserType) {
    UserType[UserType["Admin"] = 2] = "Admin";
    UserType[UserType["Teacher"] = 3] = "Teacher";
    UserType["Developer"] = "Developer";
    return UserType;
}(UserType || {});

union type

联合类型 ElementPlus.Button.type

type ButtonType = 'primary' | 'danger' | 'text' | 'link'
// [以下是 Element Plus 中 Button 的 type 类型定义](https://github.com/element-plus/element-plus/blob/dev/packages/components/button/src/button.ts#L6)

interface

接口

interface User {
    avatar: string
    id: number
    name: string
}

const user: User = {
    avatar: 'https://xx.com',
    id: 0,
    name: 'name'
}
// const user: User = {} // Type '{}' is missing the following properties from type 'User': avatar, id, name
// interface 多个类型合并 使用 extends 多个类型使用 ,区分
interface State {
    avatar: string
}

interface Method {
    setAvatar: (url: string) => void
}

interface Store extends State, Method {}

const store: Store = {
    avatar: '',
    setAvatar(url) {
        this.avatar = url
    },
}

class classStore implements Store {
    avatar: string

    constructor() {
        this.avatar = ''
    }

    setAvatar(url: string): void {
        this.avatar = url
    }
}

// interface 也能定义函数类型
interface Fn {
    (int: number): number
}

const fn: Fn = () => 1

type

类型别名

type User = {
    avatar: string
    id: number
    name: string
}
// 定义类型别名
type Admin = User
// type 多个类型合并 使用 & 多个类型就多用几次 &
type Admin = User & {
    phone: string
} & Method

// interface 也能定义函数类型
type Fn = (a: string) => void
const fn: Fn = (a) => void 0

type 和 interface 的区别

  1. 定义方式

    • 类型别名使用 type 关键字进行定义,例如:type MyType = number | string

    • 接口使用 interface 关键字进行定义,例如:interface MyInterface { prop: number }

    1. 重复定义
      • type 不能重复定义,如果多次定义同一个 type,会报错。
      • interface 可以多次定义 ,并且会将多个 interface 自动合并为一个,例如:
      interface User {
        name: string
      }
      interface User {
        age: number
      }
      const MyUser: User = {
        name: 'nicki',
        age: 41
      }
      // type MyUser = { name: string, age: number }
      
  2. 可以扩展:

    • 类型别名可以使用 & 符号进行扩展,例如:type MyType = { prop1: number } & { prop2: string },此时 MyType 的类型为 { prop1: number; prop2: string }
    • 接口可以使用 extends 关键字进行扩展,例如:interface MyInterface extends AnotherInterface { prop: number } ,此时 MyInterface 继承了AnotherInterface接口的属性和方法。
  3. 可以实现:

    • type 不能被类实现,因为 type 只是类型的别名,不会生成新的类型。
    • interface 可以被类实现,类可以通过实现 interface 来强制实现 interface 中定义的属性和方法。

总结 type 更适合用于定义复杂的类型,而 interface 更适合用于定义对象的结构和行为。在实际使用中,可以根据具体的需求选择使用 typeinterface

空值处理

ES 新增语法

?. 可选链运算符

?? 空值合并运算符

??= 逻辑空赋值

type User = {
    name: string
    sex: 0 | 1
    phone: string | null // 表示 phone 的 key 已经定义 但是 值可能是 string 或者 null
    address?: string // 表示 address 这个 key 可能未定义, 假设和后端定义好的 如果 sex 等于 0 那么 address 肯定有值

}
const user: User = {
    name: 'xx',
    sex: 1,
    phone: null
}

// 使用 ES2020 新增的可选链
{
    // ?.
    // ??
    // ??=
    // ||=
    // &&=
    user.phone?.toUpperCase()

    type OptionalChaining = {
        a: null | {
            $a: string
            b: null | {
                c: null | (() => void)
            }
            c?: {
                d: {
                    e?: number
                }
            }
        }
    }
    const optional: OptionalChaining = {
        a: null
    }
    // optional.a.b // 'optional.a' is possibly 'null'.
    const $a = optional.a?.$a
    optional?.a?.b?.c?.call(null)

    let e = optional?.a?.c?.d.e
    const hasE = (e ?? -1) !== -1
    e ??= 1
}

// 使用非空断言 这是 ts 特有的
// ! 可以使前面的值肯定不为 null 或 undefined
{
    // 例如 sex 等于 0 address 肯定有值
    if (user.sex === 0) console.log(user.address!) // 这里的 address 类型为 string 而不是 string | undefined
}

泛型

给什么就返回什么类型 不用多次定义类型

常见泛型缩写

T 表示 Type

E 表示 Element

K 表示 Key

V 表示 Value

// 常见的泛型
// numberArr 的类容只能为 number 类型 
const numberArr: Array<number> = [1, 2]

// 返回 Promise<string>
var asynchronous = () => new Promise<string>(resolve => resolve('success'))

// 返回 Promise<void>
async function asynchronous() {}

// 返回 Promise<number>
async function asynchronous() {
    return 1
}

// canvas 可能为 null 找不到这个 dom 可以使用 ! 或者 其他非空判断
const canvas = document.querySelector<HTMLCanvasElement>('#root')
const isCanvas1 = canvas instanceof HTMLCanvasElement
const isCanvas2 = canvas !== null

// 如果上面的 document.querySelector 不加泛型参数的话 ts 就没法推荐是什么类型的 HTMLElement
// 就没法使用下面 HTMLCanvasElement 对象特定的方法 比如 HTMLCanvasElement.getContext
canvas!.getContext('2d')

// document.querySelector('canvas') 如果查询语句是简单的标签 ts 能自动推断类型为 HTMLCanvasElement | null

// 可能为空
type Nullable<T> = T | null

// http 返回数据
interface HttpResponse<T> {
    msg: string
    code: 200 | 400 | 500
    data: Nullable<T> // 如果 code !== 200 的情况下 data 可能为 null
}

// 分页数据
type PaginationResponse<T> = {
    count: number
    page: number
    limit: number
    data: T[]
}

js 内置常用类型

Array<T>
const array: string[] = []

Promise<T>
const promise = () => new Promise<number>(resolve => resolve(2))

MouseEvent
const Button = () => {
    const handleClick = (e: MouseEvent) => void 0
    return <button onClick={handleClick}>click me</button>
}

HTMLElement
const video = document.querySelector<HTMLVideoElement>('video#player')!

ts 内置常用类型

type Type = {
    id: number
    sex: 0 | 1
    avatar: string
    address: string[]
    phone: string | null
}

Pick

选择部分类型

type pick = Pick<Type, 'id'> // {id: number}

Omit

删除部分类型

type omit = Omit<Type, 'id'> // {sex: 0 | 1, avatar: string, address: string[], phone: string | null}

Partial

让类型可选

type partial = Partial<Type> // {id?: number, sex?: 0 | 1, avatar?: string, address?: string[], phone?: string | null}

Required

让类型必填

type required = Required<partial> // {id: number, sex: 0 | 1, avatar: string, address: string[], phone: string | null}

Readonly

让类型只读 不能修改

type myReadonly = Readonly<Type> // {readonly id: number, readonly sex: 0 | 1, readonly avatar: string, readonly address: string[], readonly phone: string | null}
const testReadonly: myReadonly = {
    id: 0,
    sex: 1,
    avatar: '',
    address: [],
    phone: null
}
// change id
testReadonly.id = 2 // Cannot assign to 'id' because it is a read-only property.

Record

type record = Record<'id' | 'avatar', string> // {id: string, avatar: string}

Parameters

获取函数参数类型

function testType(arg1: number, arg2: string): number | void {}

type testParameters = Parameters<typeof testType> // [number, string]

ReturnType

获取函数返回值类型

function testType(arg1: number, arg2: string): number | void {}

type testReturnType = ReturnType<typeof testType> // number | void

类型断言

const nullable: number | null = null

var brackets = <number>nullable // number
var brackets = <string>nullable // string
var brackets = <bigint><string>nullable // bigint

var $as = nullable as number // number
var $as = nullable as string // string
var $as = nullable as string as bigint // bigint

const notEmpty = nullable! // number

tsconfig常见配置

/// tsconfig.json
{
  "compilerOptions": {
    // 指定类型声明文件的根文件夹路径。
    // 当指定了 typeRoots 选项时,编译器会在指定的根文件夹中查找类型声明文件,并将其包含在编译中。
    "typeRoots": [
      "./node_modules/@types/",
      "./types"
    ],
    // 严格模式
    "strict": true,
    // jsx 编译选项
    "jsx": "preserve",
    "baseUrl": "./",
    // paths 配置 ts 类型的 alias,vite 的需要单的配置
    "paths": {
      "@": [
        "src"
      ],
      "@/*": [
        "src/*"
      ]
    }
  },
  // 包含某些路径类型的编译
  "include": [
    "src/**/*.ts",
    "src/**/*.d.ts",
    "src/**/*.tsx",
    "src/**/*.vue",
    "build/**/*.ts",
    "build/**/*.d.ts",
    "vite.config.ts"
  ],
  // 忽略某些路径类型的编译
  "exclude": [
    "node_modules",
    "dist",
    "**/*.js"
  ]
}

配合 vue3 使用

base type

// 基础类型可以不使用泛型 如果默认值为 null 需要定义泛型
const name = ref<string>('nicki')
const goods = reactive<Record<'id' | 'price', string>>({id: '11', price: '20.2'})
const discountedPrice = computed<number>(() => Number.parseFloat(goods.price) * .6)

component props

interface BaseCosUploadProps {
    max?: number,
    accept?: string,
    listType?: 'text' | 'picture' | 'picture-card',
    showPercent: boolean,
    isVideo: boolean,
    multiple?: boolean,
    value: BaseFileItem[],
    showUploadList?: boolean | {
        showRemoveIcon: (boolean | ((props: Record<string, unknown>) => boolean)) & boolean,
        showPreviewIcon: (boolean | ((props: Record<string, unknown>) => boolean)) & boolean,
    },
    buttonProps?: ButtonProps,

    customRequest?(files: BaseFileItem[]): void,

    disabled?: boolean,
}

// 使用泛型定义 props 并通过编译器宏设置默认值
const props = withDefaults(defineProps<BaseCosUploadProps>(), {
    accept: 'image/*',
    listType: 'text',
    showPercent: false,
    isVideo: false,
    multiple: false,
    showUploadList: void 0,
    buttonProps: void 0,
    disabled: void 0,
})

emit

const emit = defineEmits<{
    (e: 'success', data: COS.PutObjectResult[] | BaseUploadVideoResult, originData: BaseFileItem[]): void,
    (e: 'delete', data: BaseFileItem[]): void,
    (e: 'update:value', data: BaseFileItem[]): void,
    (e: 'change', data: BaseFileItem[]): void,
    (e: 'progress', data: COS.ProgressInfo): void,
}>()

provide and inject

type CourseSectionPendingUploadListValue = {
    list: User[]
    pending: User[]
}
provide('pendingUploadList', pendingUploadListValue)
const pendingUploadList = inject<CourseSectionPendingUploadListValue>('pendingUploadList', {})

event bus

推荐使用 mitt

import mitt from 'mitt'

type Events = {
    open: undefined
    showTextEditor: Konva.Text
    hideTextEditor: undefined
    $1: Tpl1JSON
    $2: Tpl2JSON
    $3: Tpl3JSON
    $4: Tpl4JSON
    $5: Tpl5JSON
}

const eventBus = mitt<Events>() // inferred as Emitter<Events>

eventBus.on('showTextEditor', (text) => void 0) // 'text' has inferred type 'Konva.Text'

eventBus.emit('showTextEditor', 'test') // Error: Argument of type 'string' is not assignable to parameter of type 'Konva.Text'. (2345)

http

const http = axios.create()

type User = {
    avatar: string
    id: number
    name: string
}

function fetchUserInfo(id: number) {
    return http.get<User>(`/user?ID={id}`)
}

function setUserInfo(info: Partial<Omit<User, 'id'>>) {
    return http.post<null>('/user', info)
}

定义全局类型

localStorage 类型

type Nullable<T> = T | null

type Profile = {
    name: string
    avatar: string
    id: number
    address: Nullable<string>
    sex: 0 | 1
}

type AppCache = {
    token: string
    profile: Profile
}

export function getCache<K extends keyof AppCache>(key: K): Nullable<AppCache[K]> {
    const v = localStorage.getItem(key)
    return v === null ? null : JSON.parse(v) as AppCache[K]
}

export function setCache<T extends AppCache[K], K extends keyof AppCache>(key: K, value: T): T {
    localStorage.setItem(key, JSON.stringify(value))
    return value
}

export function clearCache() {
    return localStorage.clear()
}

const token = getCache('token') // Nullable<string>
const profile = getCache('profile') // Nullable<Profile>
setCache('token', true) // Argument of type 'boolean' is not assignable to parameter of type 'string'.

h5 和 app 通讯

type Methods = {
    // get app version, like 1.2.3
    getVersion: () => string
    // get file base64 string
    getFile: (path: string) => string | undefined
}

declare interface Window {
    flutter_inappwebview?: {
        callHandler: <K extends keyof Methods>(name: K, ...args: Parameters<Methods[K]>) => Promise<ReturnType<Methods[K]>>
    }
}

window.addEventListener('flutterInAppWebViewPlatformReady', async function (e): Promise<void> {
    const version = await window.flutter_inappwebview!.callHandler('getVersion')
    console.log(version) // 0.1.2

    const base64 = await window.flutter_inappwebview!.callHandler('getFile', '/cache/img1.jpg')
    console.log(base64) // data:image/jpeg;base64,xxx | undefined
})

node 环境变量

declare namespace NodeJS {
    interface ProcessEnv {
        // 环境变量
        NODE_ENV: 'development' | 'production'
        // api请求地址
        API_BASE_URL: string
    }
}

const isDevMode = process.env.NODE_ENV === 'development'