Программирование «без машинописного текста»

JavaScript TypeScript
Программирование «без машинописного текста»

Как только вы попадете в Typescript, вы не сможете обойтись без него. Чувство безопасности, умные подсказки, рефакторинг кода..

Но есть еще много старых проектов, все еще использующих JavaScript? Трудно переехать

Использование Typescript в конечном итоге преобразуется в код JavaScript. Я не хочу создавать этот набор инструментов сборки. Мне нравится писать и запускать напрямую, напрямую отлаживать и отправлять напрямую в npm.

...


Вывод типов современного VSCode для JavaScript (на основе Typescript внизу) уже очень эффективен:



Но недостаточно, недостаточно властно, недостаточно решительно, иначе зачем нам Typescript:

Вы не можете присвоить строку числу, хотелось бы, чтобы была подсказка, но нет

💡 Редактор этой статьи основан на VSCode.

💡Для этой статьи требуется определенная основа Typescript

💡Рекомендуется к просмотруоригинальный, лучшая типографика



Включить проверку типов для JavaScript

Первый шаг, сначала убедитесь, что в вашем VScode включена проверка JavaScript:



Второй шаг — добавить в начало файла// @ts-check

Bingo!VSCode уже содержит ошибку типа



Третий шаг, проверка типов

Если вы хотите программно проверить свой код на наличие ошибок типа, вы можете установить интерфейс командной строки Typescript:

$ yarn add typescript -D

Затем добавьтеjsconfig.jsonфайл, этот файл конфигурации похож наtsconfig.jsonПараметры конфигурации (см.здесь) аналогично, за исключением того, чтоjsconfigСосредоточьтесь на Javascript. Наша конфигурация выглядит следующим образом:

{
  "compilerOptions": {
    "target": "esnext",
    "noEmit": true,             // 🔴关闭输出,因为我们只进行类型检查
    "skipLibCheck": true, 
    // "checkJs": true,         // 开启对所有 JS 文件的类型检查,不需要 @ts-check
    "strict": true,             // 🔴严格的类型检查
    "moduleResolution": "node", // 🔴按照 Node 方式查找模块
    "jsx": "react",             // 🔴开启 JSX
    "module": "esnext",
    "rootDir": "src",           // 🔴源文件目录
    "resolveJsonModule": true
  },
  "exclude": ["node_modules", "tests/**/*"]
}


💡 Конечно, вы можете напрямую использоватьtsconfig.jsonТоже без проблем.tsconfig.jsonможно использовать вallowJsРазрешить обработку JavaScript, дополнительно черезcheckJsГлобальный (Эквивалентно добавлению @ts-check ко всем файлам JavaScript.), чтобы включить проверку типов для JavaScript. Если вы хотите постепенно внедрять проверку типов и миграцию на JavaScript, рекомендуется использовать @ts-check



следующий вpackage.jsonДобавить кrun script, что нам удобно выполнить:

{
  "scripts": {
    "type-check": "tsc -p jsconfig.json",
    "type-check:watch": "tsc -p jsconfig.json --watch"
  },
}

Run it!

$ yarn type-check
tsc -p jsconfig.json
src/index.js:12:1 - error TS2322: Type '"str"' is not assignable to type 'number'.

12 ins.foo = 'str';
   ~~~~~~~

Found 1 error.

error Command failed with exit code 1.



объявление прогрессивного типа

Зависит исключительно от вывода типа, недостаточно, недостаточно умный Typescript, во многих случаях нам нужно явно указать Typescript, какому типу объекта он соответствует.

В JavaScript мы можем передать[JSDoc 注解](https://jsdoc.app/)или.d.tsсделать объявление типа.

Давайте попробуем перенести идиому объявления типа из Typescript в JavaScript.



1. Переменные

1⃣ Переменная аннотация

Проще, просто поместите аннотацию типа в@type {X}, синтаксис аннотаций типов соответствует Typescript:

const str = 'string' // 自动推断
let count: number
const member: string[] = []
const users: Array<{id: string, name: string, avatar?: string}> = []
let maybe: string | undefined
const str = 'string'; // 自动推断
/** @type {number} */
let count;
/** @type number ❤️ 括号可以省略,省得更简洁*/
let count;
/** @type {string[]} */
const member = [];
/** @type {Array<{ id: string, name: string, avatar?: string }>} */
const users = [];
/** @type string | undefined */
let maybe


2⃣ Утверждение типа

var numberOrString: number | string = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = numberOrString as number
/**
 * @type {number | string}
 */
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString); // 注意括号是必须的


2. Определение типа

Некоторые типы используются в нескольких местах, мы обычно передаемinterfaceилиtypeОпределите тип и повторно используйте его

1️⃣ Общий интерфейс

interface User {
  id: string;
  name: string;
  age: number;
  avatar?: string;
}
/**
 * JSDoc 注解通常是单行的
 * @typedef {{ id: string name: string age: number avatar?: string }} User 用户
 */

/** @type {User} */
var a

// 🔴不过多行也是可以被识别的
/**
 * @typedef {{
 *  id: string
 *  name: string
 *  age: number
 *  avatar?: string
 * }} User
 */

Существует еще один, более «JSDoc» способ определения типов, преимущество которого заключается в том, что вы можете ввести текст в каждое поле, что полезно для редактора intellisense или документации:

/**
  * @typedef {Object} User
  * @property {string} id 主键
  * @property {string} name 名称
  * @property {number} age 年龄
  * @property {string} [avatar] 头像
  */


2️⃣ Дженерики

// 泛型
interface CommonFormProps<T> {
  value?: T;
  onChange(value?: T): void;
}

// 多个泛型
interface Component<Props, State> {
  props: Props;
  state: State;
}
// 泛型
/**
 * @template T
 * @typedef {{
 *  value?: T
 *  onChange(value?: T): void
 * }} CommonFormProps
 */

 /** @type {CommonFormProps<string>} */
 var b

// 多个泛型
/**
 * @template Props, State
 * @typedef {{
 *  props: Props;
 *  state: State;
 * }} Component
 */

/** @type {Component<number, string>} */
var d


3️⃣ Комбинируйте существующие типы

interface MyProps extends CommonFormProps<string> {
  title?: string
}
/**
 * @typedef {{title?: string} & CommonFormProps<string>} MyProps
 */

/** @type {MyProps}*/
var e


4️⃣ Введите псевдонимы

type Predicate = (data: string, index?: number) => boolean;
type Maybe<T> = T | null | undefined;
// 类型别名
/**
 * @typedef {(data: string, index?: number) => boolean} Predicate
 */

 /** @type {Predicate} */
 var f

/**
 * @template T
 * @typedef {(T | null | undefined)} Maybe
 */

/** @type {Maybe<string>} */
var g

💡 Если нет泛型变量, несколько@typedefВ комментарии можно написать:

/**
 * @typedef {(data: string, index?: number) => boolean} Predicate
 * @typedef {{
 *  id: string
 *  name: string
 *  age: number
 *  avatar?: string
 * }} User
 */
// 有泛型变量需要单独存在
/**
 * @template T
 * @typedef {(T | null | undefined)} Maybe
 */


Как и выше, за исключением некоторых специальных синтаксисов @typedef и @template, остальные в основном такие же, как Typescript.


🙋🏻‍♂️ А вот и вопрос!

  • Как разделить типы с другими файлами?
  • Вы думаете, что написание немного многословно
  • Инструменты форматирования кода не поддерживают форматирование комментариев
  • ...


5️⃣ Декларационный документ


На самом деле мы можем импортировать определения типов из другого модуля через import :

// @file user.js
/**
 * @typedef {{
 *  id: string
 *  name: string
 *  age: number
 *  avatar?: string
 * }} User
 */
// @file index.js
/** @type {import('./user').User} */

Лучше создать отдельный*.d.ts (Файл объявления чистого типа Typescript) для хранения объявлений типов, таких какtypes.d.ts, определите и экспортируйте тип:

// @file types.d.ts

export interface User {
  id: string;
  name: string;
  age: number;
  avatar?: string;
}

export interface CommonFormProps<T> {
  value?: T;
  onChange(value?: T): void;
}

export interface Component<Props, State> {
  props: Props;
  state: State;
}

export interface MyProps extends CommonFormProps<string> {
  title?: string
}

export type Predicate = (data: string, index?: number) => boolean;
export type Maybe<T> = T | null | undefined;
/** @type {import('./types').User} */
const a = {};

/** @type {import('./types').CommonFormProps<string>} */
var b;

/** @type {import('./types').Component<number, string>} */
var e;

/** @type {import('./types').MyProps}*/
var f;

/** @type {import('./types').Predicate} */
var g;

/** @type {import('./types').Maybe<string>} */
var h;

💡 Если на тип ссылаются несколько раз, повторение импорта довольно многословно, вы можете импортировать их все сразу в начало файла:

/**
 * @typedef {import('./types').User} User
 * @typedef {import('./types').Predicate} Predicate
 */
/** @type {User} */
var a;
/** @type {Predicate} */
var g;


6️⃣Декларация третьей стороны

Да, import('module').type также может импортировать типы сторонних библиотек.. Эти библиотеки должны иметь объявления типов.Некоторые библиотеки имеют свои собственные файлы объявлений в пакете npm (например, Vue), а некоторые вам нужно загрузить соответствующие файлы объявлений @types/* (например, React, вам нужно установить @types / реагировать).

Возьмите React в качестве примера, вам нужно установить@types/react :

/**
 * @typedef {{
 *    style: import('react').CSSProperties
 * }} MyProps
 */

// 因为 @types/* 默认会暴露到全局,比如 @types/react 暴露了 React 命名空间,因此下面这样写也是可以的:
/**
 * @typedef {{
 *    style: React.CSSProperties
 * }} MyProps
 */


7️⃣Файл глобальной декларации

В дополнение к этому мы также можем объявлять типы глобально, и эти определения типов можно использовать везде в проекте. По соглашению мы создаем корневой каталог проекта (каталог, в котором находится jsconfig.json)global.d.ts

// @file global.d.ts
/**
 * 全局类型定义
 */
interface GlobalType {
  foo: string;
  bar: number;
}

/**
 * 扩展已有的全局对象
 */
interface Window {
  __TESTS__: boolean;
  // 暴露 jquery 到 window, 需要安装 @types/jquery
  $: JQueryStatic;
}
/** @type {GlobalType} */
var hh                       // ✅
window.__TESTS__             // ✅
const $elm = window.$('#id') // ✅


3. Функция

Далее давайте посмотрим, как вводить объявления для функций:

1️⃣Дополнительные параметры

function buildName(firstName: string, lastName?: string) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}

function delay(time = 1000): Promise<void> {
  return new Promise((res) => setTimeout(res, time));
}
// 🔴 JSDoc 注释风格
/**
 * @param {string} firstName 名
 * @param {string} [lastName] 姓,方括号是 JSDoc 可选参数的写法
 * @returns string 可选,Typescript 可以推断出来,如果无法推断,可以显示声明
 */
function buildName(firstName, lastName) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}
buildName('ivan') // ✅

/**
 * @param {number} [time=1000] 延迟时间, 单位为 ms
 * @returns {Promise<void>}
 */
function delay(time = 1000) {
  return new Promise((res) => setTimeout(res, time));
}
// 🔴你也可以使用 Typescript 风格, **不过不推荐!**它有以下问题:
// - 不能添加参数注释说明, 或者说工具不会识别
// - 对可选参数的处理有点问题, 和 Typescript 行为不一致
/** @type {(firstName: string, lastName?: string) => string} */
function buildName(firstName, lastName) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}

// ❌ 因为TS 将buildName 声明为了 (firstName: string, lastName: string | undefined) => string
buildName('1') 
// 🔴另一个可选参数的声明方法是 -- 显式给可选参数设置默认值(ES6的标准),TS 会推断为可选

/**
 * @param {string} firstName 名
 * @param {string} [lastName=''] 姓,方括号是 JSDoc 可选参数的写法
 * @returns string 可选,Typescript 可以推断出来,如果无法推断,可以显示声明
 */
function buildName(firstName, lastName = '' ) {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}

/** @type {(firstName: string, lastName?: string) => string} */
function buildName(firstName, lastName = '') {
  if (lastName) return firstName + ' ' + lastName;
  else return firstName;
}
buildName('1') // ✅



2️⃣ Остальные параметры

function sum(...args: number[]): number {
  return args.reduce((p, c) => p + c, 0);
}
/**
 * @param  {...number} args 
 */
function sum(...args) {
  return args.reduce((p, c) => p + c, 0);
}


3️⃣ Общие и общие ограничения

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}
/**
 * @template T
 * @template {keyof T} K // 可以在 {} 中约束泛型变量的类型
 * @param {T} obj
 * @param {K} key
 */
function getProperty(obj, key) {
  return obj[key];
}


4️⃣ это объявление параметра

interface User {
  name: string;
  lastName: string;
}
const user = {
  name: 'ivan',
  lastName: 'lee',
  say(this: User) {
    return `Hi, I'm ${this.name} ${this.lastName}`;
  },
};
/**
 * @typedef {{
 *   name: string;
 *   lastName: string;
 * }} User
 */
const user = {
  name: 'ivan',
  lastName: 'lee',
  /** @this {User} */
  say() {
    return `Hi, I'm ${this.name} ${this.lastName}`;
  },
};

Здесь в основном рассмотрены основные сценарии использования функций JavaScript, а остальные не будут расширяться по аналогии.



4. Класс

1️⃣ Регулярное использование

class Animal {
  private name: string;
  constructor(theName: string) {
    this.name = theName;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

Вы можете использовать @Public, @Private, @protected, чтобы объявить доступность поля или метода.

// 🔴 如果目标环境支持 ES6 class
class Animal {
  /**
   * @param {string} theName
   */
  constructor(theName) {
    // 属性声明
    /**
     * @type {string}
     * @private 声明为私有, 对应的也有 @protected, 默认都是 @public
     * 当然也可以使用 ES6 的 private field 语言特性
     */
    this.name = theName;
  }
  /**
   * @param {number} [distanceInMeters]
   */
  move(distanceInMeters = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}

const a = new Animal('foo');
a.name; // ❌ 不能访问私有字段
// 🔴 如果目标环境不支持,只能使用 函数形式了
/**
 * @constructor
 * @param {string} theName
 */
function Animal1(theName) {
  /**
   * @type {string} 这里不能使用 private 指令
   */
  this.name = theName;
}

/**
 * @param {number} [distanceInMeters = 0]
 */
Animal1.prototype.move = function (distanceInMeters) {
  console.log(this.name + ' moved ' + (distanceInMeters || 0) + 'm.');
};

const a = Animal1('bird') // ❌ 使用 @constructor 后,只能 new 调用



2️⃣ Дженерики

import React from 'react';

 
export interface ListProps<T> {
  datasource: T[];
  idKey?: string;
}

export default class List<T> extends React.Component<ListProps<T>> {
  render() {
    const { idKey = 'id', datasource } = this.props;
    return datasource.map(
      (i) => React.createElement('div', { key: i[idKey] }) /*...*/,
    );
  }
}
// @ts-check
import React from 'react';

/**
 * @template T
 * @typedef {{
 *  datasource: T[];
 *  idKey?: string;
 * }} ListProps
 */

/**
 * @template T
 * @extends {React.Component<ListProps<T>>} 使用 extends 声明继承类型
 */
export default class List extends React.Component {
  render() {
    const { idKey = 'id', datasource } = this.props;
    return datasource.map(
      // @ts-expect-error JavaScript 中也可以使用 @ts-ignore 等注释指令
      (i) => React.createElement('div', { key: i[idKey] }) /*...*/,
    );
  }
}

// 显式定义类型
/** @type {List<{id: string}>} */
var list1;
// 自动推断类型
var list2 = <List datasource={[{ id: 1 }]}></List>;

⚠️ Не поддерживает общие значения переменных по умолчанию.



3️⃣Реализация интерфейса

interface Writable {
  write(data: string): void
}

class Stdout implements Writable {
  // @ts-expect-error ❌ 这里会报错,data 应该为 string
  write(data: number) {} 
}
/**
 * @typedef {{
 *   write(data: string): void
 * }} Writable
 */

/**
 * @implements {Writable}
 */
class Output {
  /**
   * @param {number} data 
   */
  write(data) { // ❌ data 应该为 string
  }
}


6. Другие

перечислить

@enum можно использовать только для ограничения типа члена объекта, он бесполезен как тип

/** @enum {number} Mode*/
const Mode = {
  Dark: 1,
  Light: 2,
  Mixed: 'mix' // ❌
};

/**
 * @type {Mode}
 */
var mode = 3; // ✅,作为类型时,作用不大


@deprecated

class Foo {
  /** @deprecated */
  bar() {
  }
}

(new Foo).bar // ❌ 在 Typescript 4.0 会报错


Суммировать

«Программирование без машинописного текста» — это что-то вроде титульной вечеринки На самом деле дело не в том, что здесь не используется машинописный текст, а в том, что машинописный текст используется неинвазивным способом.

Способ использования аннотаций JSDoc для объявления типа может в основном соответствовать различным требованиям проверки типов JavaScript, конечно, есть много функций Typescript, которые не поддерживаются.

Таким образом, используя различные бонусы, которые дает Typescript, кстати, вырабатывайте привычку писать комментарии самостоятельно, а комментарии JSDoc также удобны для генерации документов. почему бы нет



В этой статье упоминаются следующие аннотации JSDoc:

  • @type объявляет типы сущностей, такие как переменные, члены класса
  • @typedef определяет тип
  • @template объявляет общие переменные
  • @param определяет тип параметра функции
  • @returns определяет тип возвращаемого значения функции
  • @constructor или @class объявляют функцию конструктором
  • @extends объявляет наследование классов
  • @implements объявляет, какие интерфейсы реализует класс
  • @public, @private, @protected объявляют доступность члена класса
  • Тип члена объекта ограничения @enum
  • @deprecated объявляет объект устаревшим
  • @property и @typedef работают вместе, чтобы объявить тип поля

В дополнение к этой статье иОфициальная документация машинописного текстаАннотации, отличные от упомянутых аннотаций JSDoc, временно не поддерживаются.



Далее вы можете взглянуть на официальный TypescriptДокументация, продолжить изучениеJSDoc, а также следующие варианты использования (дополнения приветствуются):

Если вы исследуете другие способы игры, дайте мне знать в комментариях.



расширять