Once you’re stuck in Typescript, you can’t live without it. Security, smart tips, code refactoring..
But there are a lot of old projects that still use JavaScript? It’s going to be hard to migrate
With Typescript you still end up translating into JavaScript code. I don’t want to use a whole set of build tools. I like to write and run, debug directly, and post directly to NPM.
.
Modern VSCode type inference for JavaScript (underlying Typescript) is pretty awesome:
But it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough, it’s not enough.
String can’t be assigned to number. We wish there was a hint, but there isn’t
💡 the article editor is based on VSCode
💡 This article requires some Typescript basics
💡 recommended to view the original, better typesetting
Turn on type checking for JavaScript
First, make sure your VScode has JavaScript validation enabled:
Second, add at the top of the file// @ts-check
Bingo! VSCode already has a type error
Step 3: Type checker
If you want to programmatically verify that your code has type errors, install Typescript CLI:
$ yarn add typescript -D
Copy the code
Json file, similar to tsconfig.json, with similar configuration parameters (see here), except that JsConfig is Javascript focused. Our configuration is as follows:
{
"compilerOptions": {
"target": "esnext"."noEmit": true.// 🔴 turns off the output because we are only doing type checking
"skipLibCheck": true.// "checkJs": true, // enable type check for all JS files without @ts-check
"strict": true.// 🔴 Strict type checking
"moduleResolution": "node".// 🔴 find modules in Node mode
"jsx": "react".// 🔴 Enable JSX
"module": "esnext"."rootDir": "src".// 🔴 Source file directory
"resolveJsonModule": true
},
"exclude": ["node_modules"."tests/**/*"]}Copy the code
💡 you can also use tsconfig.json directly. You can use allowJs in tsconfig.json to allow JavaScript processing, and you can turn on JavaScript type checking by checkJs globally (equivalent to adding @ts-check to all JavaScript files). If you want to progressively expand type checking and migration to JavaScript, use @ts-check
Next add the run script to package.json so we can execute:
{
"scripts": {
"type-check": "tsc -p jsconfig.json"."type-check:watch": "tsc -p jsconfig.json --watch"}},Copy the code
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.
Copy the code
Progressive type declarations
Relying solely on type inference is not enough. Typescript is not smart enough, and in many cases we need to explicitly tell Typescript what type the entity is.
In JavaScript, type declarations can be made via [JSDoc annotations](https://jsdoc.app/) or.d.ts.
Here’s an attempt to migrate the Typescript conventions of type declarations to JavaScript.
1. The variable
1 Notes on one variable
It’s easy to put type annotations in @type {X}. Type annotations have the same syntax as Typescript:
const str = 'string' // Automatic inference
let count: number
const member: string[] = []
const users: ArrayThe < {id: string.name: string, avatar? :string} > = []let maybe: string | undefined
Copy the code
const str = 'string'; // Automatic inference
/ * *@type {number} * /
let count;
/ * *@type Number ❤️ parentheses can be omitted to save more conciseness */
let count;
/ * *@type {string[]} * /
const member = [];
/ * *@type {Array<{ id: string, name: string, avatar? : string }>} * /
const users = [];
/ * *@type string | undefined */
let maybe
Copy the code
2 Type assertion
var numberOrString: number | string = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = numberOrString as number
Copy the code
/ * * *@type {number | string}* /
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = / * *@type {number} * / (numberOrString); // Note that parentheses are required
Copy the code
Type definition
Some types can be used in more than one place, and we usually define a type by interface or type and reuse it
1 ️ ⃣ conventional interface
interface User {
id: string;
name: string;
age: number; avatar? :string;
}
Copy the code
/** * JSDoc annotations are usually single-line *@typedef {{ id: string name: string age: number avatar? : string }} User User */
/ * *@type {User} * /
var a
// 🔴 but multiple lines can also be recognized
/ * * *@typedef {{ * id: string * name: string * age: number * avatar? : string * }} User
*/
Copy the code
There is another, more ‘JSDoc’ type definition which has the advantage that you can specify each field with text, useful for editor intelligence hints or document generation:
/ * * *@typedef {Object} User
* @property {string} * id primary key@property {string} Name the name *@property {number} The age age *@property {string} [avatar] * /
Copy the code
2 ️ ⃣ generics
/ / generics
interfaceCommonFormProps<T> { value? : T; onChange(value? : T):void;
}
// Multiple generics
interface Component<Props, State> {
props: Props;
state: State;
}
Copy the code
/ / generics
/ * * *@template T
* @typedef {{ * value? : T * onChange(value? : T): void * }} CommonFormProps
*/
/ * *@type {CommonFormProps<string>} * /
var b
// Multiple generics
/ * * *@template Props, State
* @typedef {{ * props: Props; * state: State; *}} Component
*/
/ * *@type {Component<number, string>} * /
var d
Copy the code
3 Existing types of ️ composition
interface MyProps extendsCommonFormProps<string> { title? :string
}
Copy the code
/ * * *@typedef {{title? : string} & CommonFormProps<string>} MyProps
*/
/ * *@type {MyProps}* /
var e
Copy the code
4️ nickname of type
type Predicate = (data: string, index? :number) = > boolean;
type Maybe<T> = T | null | undefined;
Copy the code
// Type alias
/ * * *@typedef {(data: string, index? : number) => boolean} Predicate* /
/ * *@type {Predicate} * /
var f
/ * * *@template T
* @typedef {(T | null | undefined)} Maybe* /
/ * *@type {Maybe<string>} * /
var g
Copy the code
💡 Without generic variables, multiple @typedefs can be written in a single comment:
/ * * *@typedef {(data: string, index? : number) => boolean} Predicate
* @typedef {{ * id: string * name: string * age: number * avatar? : string * }} User
*/
// There are generic variables that need to stand alone
/ * * *@template T
* @typedef {(T | null | undefined)} Maybe* /
Copy the code
As mentioned above, except for the @typedef and @template syntax, the rest is basically Typescript.
🙋🏻♂️ then the question is!
- How do I share types with other files?
- You think it’s a little wordy
- The code formatting tool does not support formatting comments
- .
5️ announcement document
In fact, we can import a type definition from another module by importing:
// @file user.js
/ * * *@typedef {{ * id: string * name: string * age: number * avatar? : string * }} User
*/
Copy the code
// @file index.js
/ * *@type {import('./user').User} * /
Copy the code
A better approach is to create a separate *.d.ts (Typescript pure type declaration file) to store type declarations, such as types.d.ts, and define and export types:
// @file types.d.ts
export interface User {
id: string;
name: string;
age: number; avatar? :string;
}
export interfaceCommonFormProps<T> { value? : T; onChange(value? : T):void;
}
export interface Component<Props, State> {
props: Props;
state: State;
}
export interface MyProps extendsCommonFormProps<string> { title? :string
}
export type Predicate = (data: string, index? :number) = > boolean;
export type Maybe<T> = T | null | undefined;
Copy the code
/ * *@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;
Copy the code
💡 If a type is referenced more than once and repeating the import is verbose, you can import them at the top of the file once:
/ * * *@typedef {import('./types').User} User
* @typedef {import('./types').Predicate} Predicate* /
/ * *@type {User} * /
var a;
/ * *@type {Predicate} * /
var g;
Copy the code
6️ third party statement document
Yes, import(‘module’).type can also import third-party library types. These libraries require type declarations, some of which come with the NPM package (e.g. Vue), while others require you to download the corresponding @types/* declarations (e.g. React).
Use React as an example to install @types/ React:
/ * * *@typedef {{ * style: import('react').CSSProperties * }} MyProps
*/
// Since @types/* is exposed globally by default, such as @types/react exposes the react namespace, it is also ok to write:
/ * * *@typedef {{ * style: React.CSSProperties * }} MyProps
*/
Copy the code
7️ global announcement document
In addition, we can declare types globally and use these type definitions everywhere in the project. As usual, we create a global.d.ts in the project root directory (where jsconfig.json is located)
// @file global.d.ts
/** * Global type definition */
interface GlobalType {
foo: string;
bar: number;
}
/** * extends an existing global object */
interface Window {
__TESTS__: boolean;
// To expose jquery to window, install @types/jquery
$: JQueryStatic;
}
Copy the code
/ * *@type {GlobalType} * /
var hh / / ✅
window.__TESTS__ / / ✅
const $elm = window$('#id') / / ✅
Copy the code
3. The function
Let’s see how to declare a function as a type:
1️ optional parameter
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));
}
Copy the code
// 🔴 JSDoc comment style
/ * * *@param {string} FirstName name *@param {string} [lastName] lastName with square brackets for optional JSDoc arguments *@returns String is optional, Typescript can infer this, and if it cannot, it displays the declaration */
function buildName(firstName, lastName) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
buildName('ivan') / / ✅
/ * * *@param {number} [time=1000] Delay time, expressed in ms *@returns {Promise<void>}* /
function delay(time = 1000) {
return new Promise((res) = > setTimeout(res, time));
}
Copy the code
// 🔴 you can also use Typescript style, ** but not recommended! ** It has the following problems:
// - Cannot add parameter comments, or the tool does not recognize them
// - The handling of optional arguments is a bit inconsistent with Typescript behavior
/ * *@type {(firstName: string, lastName? : string) => string} * /
function buildName(firstName, lastName) {
if (lastName) return firstName + ' ' + lastName;
else return firstName;
}
/ / ❌ because TS buildName statement to (firstName: string, lastName: string | undefined) = > string
buildName('1')
Copy the code
// 🔴 Another way to declare optional arguments is to explicitly set default values (ES6 standard) for optional arguments, which TS will infer to be optional
/ * * *@param {string} FirstName name *@param {string} [lastName= "] lastName with square brackets for optional JSDoc arguments *@returns String is optional, Typescript can infer this, and if it cannot, it displays the declaration */
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') / / ✅
Copy the code
2️ residual parameter
function sum(. args:number[]) :number {
return args.reduce((p, c) = > p + c, 0);
}
Copy the code
/ * * *@param {... number} args
*/
function sum(. args) {
return args.reduce((p, c) = > p + c, 0);
}
Copy the code
3️ generalization and generalization constraint
function getProperty<T.K extends keyof T> (obj: T, key: K) {
return obj[key];
}
Copy the code
/ * * *@template T
* @template {keyof T} K // can constrain the type * of a generic variable in {}@param {T} obj
* @param {K} key* /
function getProperty(obj, key) {
return obj[key];
}
Copy the code
4️ parameter statement
interface User {
name: string;
lastName: string;
}
const user = {
name: 'ivan'.lastName: 'lee'.say(this: User) {
return `Hi, I'm The ${this.name} The ${this.lastName}`; }};Copy the code
/ * * *@typedef {{ * name: string; * lastName: string; *}} User
*/
const user = {
name: 'ivan'.lastName: 'lee'./ * *@this {User} * /
say() {
return `Hi, I'm The ${this.name} The ${this.lastName}`; }};Copy the code
This basically covers the basic usage scenarios of JavaScript functions, and the rest goes on and on.
Class 4.
1️ conventional usage
class Animal {
private name: string;
constructor(theName: string) {
this.name = theName;
}
move(distanceInMeters: number = 0) {
console.log(`The ${this.name} moved ${distanceInMeters}m.`); }}Copy the code
You can declare the accessibility of fields or methods using @public, @private, or @protected.
🔴 if the target environment supports ES6 class
class Animal {
/ * * *@param {string} theName* /
constructor(theName) {
// Attribute declaration
/ * * *@type {string}
* @private Declared private, and the corresponding@protected, the default is@public* You can also use ES6's private Field language feature */
this.name = theName;
}
/ * * *@param {number} [distanceInMeters]
*/
move(distanceInMeters = 0) {
console.log(`The ${this.name} moved ${distanceInMeters}m.`); }}const a = new Animal('foo');
a.name; // ❌ cannot access private fields
Copy the code
// 🔴 If the target environment does not support it, you can only use functional form
/ * * *@constructor
* @param {string} theName* /
function Animal1(theName) {
/ * * *@type {string} The private directive */ cannot be used here
this.name = theName;
}
/ * * *@param {number} [distanceInMeters = 0]
*/
Animal1.prototype.move = function (distanceInMeters) {
console.log(this.name + ' moved ' + (distanceInMeters || 0) + 'm.');
};
const a = Animal1('bird') // ❌ can only be called new with @constructor
Copy the code
2 ️ ⃣ generics
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] }) / *... * /,); }}Copy the code
// @ts-check
import React from 'react';
/ * * *@template T
* @typedef {{ * datasource: T[]; * idKey? : string; *}} ListProps
*/
/ * * *@template T
* @extends {React.Component<ListProps<T>>} Extends type */ using the extends declaration
export default class List extends React.Component {
render() {
const { idKey = 'id', datasource } = this.props;
return datasource.map(
// @ts-expect-error Comments such as @ts-ignore can also be used in JavaScript
(i) = > React.createElement('div', { key: i[idKey] }) / *... * /,); }}// Explicitly define the type
/ * *@type {List<{id: string}>} * /
var list1;
// Automatically infer the type
var list2 = <List datasource={[{ id: 1}}] ></List>;
Copy the code
⚠️ does not support generic variable defaults
3️ interface realization
interface Writable {
write(data: string) :void
}
class Stdout implements Writable {
// @ts-expect-error ❌
write(data: number){}}Copy the code
/ * * *@typedef {{ * write(data: string): void * }} Writable
*/
/ * * *@implements {Writable}* /
class Output {
/ * * *@param {number} data
*/
write(data) { // ❌ data should be string}}Copy the code
6. Other
The enumeration
@enum can only be used to constrain the member type of an object
/ * *@enum {number} Mode*/
const Mode = {
Dark: 1.Light: 2.Mixed: 'mix' / / ❌
};
/ * * *@type {Mode}* /
var mode = 3; // ✅ is not very useful as a type
Copy the code
@deprecated
class Foo {
/ * *@deprecated * /
bar(){}},new Foo).bar ❌ errors are reported in Typescript 4.0
Copy the code
conclusion
“Typescript-free programming” is a bit of a title party, but instead of using Typescript, we’re using Typescript in a non-intrusive way.
JSDoc annotations are used to declare types in a way that meets most of JavaScript’s type-checking requirements, but many Typescript features don’t.
This way, JSDoc annotations can also be used for document generation by taking advantage of the benefits of Typescript and developing the habit of writing your own comments. Why not
This article refers to the following JSDoc annotations:
- @type declares entity types, such as variables and class members
- @typedef Defines a type
- @template declares generic variables
- @param defines function parameter types
- Returns Defines the return value type of a function
- @constructor or @class declare a function to be a constructor
- @extends declares class inheritance
- @implements declares which interfaces the class implements
- @public, @private, and @protected declare the accessibility of class members
- @enum Indicates the member type of the constraint object
- @deprecated Deprecates an entity
- The @property and @typedef combine to declare the type of a field
Annotations other than the JSDoc annotations mentioned in this article and the Typescript official documentation are not currently supported.
Next, take a look at the Typescript documentation to explore JSDoc and the following use cases (welcome to add) :
- Typescript JSDoc Playground
- babel-plugin-trim-helpers
If you discover more, let me know in the comments.
extension
- Typescript: Type Checking JavaScript Files
- Typescript: JSDoc Reference
- Typescript: TSConfig Reference
- VSCode: Working with JavaScript
- VSCode: jsconfig.json
- JSDoc