TypeScript
Learn how to write codemods to modify common TypeScript. This guide will teach you how to update variable and interface types using jscodeshift.
If you're looking to modify TypeScript code in bulk, jscodeshift can be incredibly helpful. In this guide, we'll explore how you can use jscodeshift to modify common TypeScript syntaxes. Whether you're looking to update the types of variables or interfaces, this guide will provide you with the knowledge and tools you need to get started.
Remember to use the ts
or tsx
parser when modifying TypeScript files.
Types
TypeScript type aliases allow you to give a name to a specific type or combination of types, which can be reused throughout your code. A type alias is like a shortcut that allows you to define a new name for a more complex type or set of types, making your code more concise and easier to read.
In jscodeshift, these are represented by the node: TSTypeAliasDeclaration
.
Creating a type annotation
If you wanted to construct a new type, you could do so using one of the TypeScript's primitive types.
j.tsBooleanKeyword()
:boolean
j.tsStringKeyword()
:string
j.tsNumberKeyword()
:number
In addition, TypeScript provides a range of basic types.
j.tsNullKeyword()
:null
j.tsAnyKeyword()
:any
j.tsUnknownKeyword()
:unknown
j.tsVoidKeyword()
:void
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
// Build a new type
const newType = j.tsTypeAliasDeclaration(
j.identifier('Potato'), // type name "potato"
j.tsBooleanKeyword() // boolean type annotation
);
// Insert it at the top of the document
source.get().node.program.body.unshift(newType);
return source.toSource();
}
Output:
type Potato = boolean;
Union types
As we know with TypeScript, it's possible for different type annotations to be combined using union types.
These are represented with the j.TSUnionType
node.
To construct a union type containing two arbitrary strings, you could do the following.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
// Build a new type
const newType = j.tsTypeAliasDeclaration(
j.identifier('Potato'), // type name "potato"
// Create a union type with two components
j.tsUnionType([
j.tsLiteralType(j.stringLiteral('foo')),
j.tsLiteralType(j.stringLiteral('bar'))
])
);
// Insert it at the top of the document
source.get().node.program.body.unshift(newType);
return source.toSource();
}
Output:
type Potato = 'foo' | 'bar';
Arrays
Similary, TypeScript arrays can be constructed
by using the j.tsArrayType()
and passing in one of the primitive types mentioned above.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
// Build a new type
const newType = j.tsTypeAliasDeclaration(
j.identifier('Potato'), // type name "potato"
j.tsArrayType(j.tsStringKeyword()) // array of strings type annotation
);
// Insert it at the top of the document
source.get().node.program.body.unshift(newType);
return source.toSource();
}
Output:
type Potato = string[];
Interfaces
A TypeScript interface
is known as a TSInterfaceDeclaration
. These can me found and modified the same as any other node.
Renaming an interface
For example, if you wanted to modify the name of a particular interface you could do the following.
const oldName = 'Lunch';
const newName = 'Breakfast';
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.TSInterfaceDeclaration, { id: { name: oldName } }) // Find all TSInterfacDeclarations with the name "Lunch"
.forEach((path) => (path.node.id.name = newName)); // Replace it with "Breakfast"
return source.toSource();
}
Input:
interface Lunch {
cheese: string;
burger: number;
}
Output:
-interface Lunch {
+interface Breakfast {
cheese: string;
burger: number;
}
Adding interface properties
A property of interface is known as a TSPropertySignature
, representing the individual members which make up the interface
itself.
TSPropertySignatures
simply wrap a TSTypeAnnotation
which we have already seen in above.
Adding a property to an existing interface
includes modifying the body
array of the InterfaceDeclaration
.
For example, to add icecream: string
you could do the following.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.TSInterfaceDeclaration, { id: { name: oldName } }) // Find all TSInterfacDeclarations with the name "Lunch"
.forEach((path) => {
// Insert a new property called 'icecream' with a `string` primitive type
path.node.body.body = [
...path.node.body.body,
j.tsPropertySignature(
j.identifier('icecream'),
j.tsTypeAnnotation(j.tsStringKeyword())
)
];
});
return source.toSource();
}
Input:
interface Lunch {
cheese: string;
burger: number;
}
Output:
interface Lunch {
cheese: string;
burger: number;
+ icecream: string;
}
Modifying interface properties
Modifying interface properties can be a lot more straightforward since you can simply
filter by the interface and property name, then simply replace the typeAnnotation
.
For example, if we wanted to replace the TSTypeAnnotation
of the icecream
property with
a string literal type vanilla
instead of a string
we could do the following.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.TSInterfaceDeclaration, { id: { name: 'Lunch' } }) // Find all TSInterfacDeclarations with the name "Lunch"
.find(j.TSPropertySignature, { key: { name: 'icecream' } }) // Find all TSPropertySignatures with the name "icecream"
.forEach((path) => {
// Replace the type annotation with a string literal type 'vanilla'
path.node.typeAnnotation = j.tsTypeAnnotation(
j.tsLiteralType(j.stringLiteral('vanilla'))
);
});
return source.toSource();
}
Input:
interface Lunch {
cheese: string;
burger: number;
icecream: string;
}
Output:
interface Lunch {
cheese: string;
burger: number;
+ icecream: 'vanilla';
}
Optional properties
In TypeScript, interface properties can be marked as optional
with the ?
keyword.
The same can be done in a codemod by setting the optional
argument when creating the node.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.TSInterfaceDeclaration, { id: { name: 'Lunch' } }) // Find all TSInterfacDeclarations with the name "Lunch"
.forEach((path) => {
// Insert a new property called 'icecream' with a `string` primitive type
path.node.body.body = [
...path.node.body.body,
j.tsPropertySignature(
j.identifier('icecream'),
j.tsTypeAnnotation(j.tsStringKeyword())
),
true // specifies that the icecream property is optional
];
});
return source.toSource();
}
Input:
interface Lunch {
cheese: string;
burger: number;
}
Output:
interface Lunch {
cheese: string;
burger: number;
+ icecream?: string;
}
Extending other interfaces
Interfaces can extend other interfaces. As an AST these are represented as
an array of TSExpressionWithTypeArguments
on the extends
property of an interface
.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.TSInterfaceDeclaration, { id: { name: oldName } }) // Find all TSInterfacDeclarations with the name "Lunch"
.forEach(
(path) =>
(path.node.extends = [
// Replace the extisting extends property
j.tsExpressionWithTypeArguments(j.identifier('Snacks')) // Create a new `TSExoressionWithTypeArguments` array
])
);
return source.toSource();
}
Input:
interface Snacks {
fries: string;
}
interface Lunch {
cheese: string;
burger: number;
icecream: string;
}
Output:
interface Snacks {
fries: string;
}
+interface Lunch extends Snacks {
cheese: string;
burger: number;
icecream: 'vanilla';
}
Annotations
Adding types to variables
Adding type annotations to an existing VariableDeclarator
involves assigning a type node
to the typeAnnotation
property.
export default function transformer(file, { jscodeshift: j }, options) {
const source = j(file.source);
source
.find(j.VariableDeclarator)
.find(j.Identifier, { name: 'dog' }) // Filter by name === 'dog'
// Add a typeAnnotation property to the node
.forEach(
(path) =>
(path.node.typeAnnotation = j.tsTypeAnnotation(j.tsStringKeyword()))
);
return source.toSource();
}
Input:
const dog = 'Poodle';
Output:
-const dog = 'Poodle';
+const dog: string = 'Poodle';