TypeScript Basics #7: Utility Types and tsconfig

5 min read

Last time we covered the deeper tools of generics. This is the final post in the series. We wrap up with two topics — the standard utility types you’ll use every day in real code, and the key options of tsconfig.json that control compile behavior.

What are utility types? #

TypeScript ships built-in utility types for the type-transformation patterns used most often. You can use them anywhere without an import.

Utility type examples
type User = { id: string; name: string; age: number; email: string };

type UserPreview = Pick<User, 'id' | 'name'>;
//   = { id: string; name: string }

type UserUpdate = Partial<User>;
//   = { id?: string; name?: string; age?: number; email?: string }

These transformations are built with tools like the mapped types and indexed access we saw in #6 — they’re expressible in TypeScript itself. You could write them yourself, but the common ones are in the standard library, so use those.

Let’s look at the headline ones by category.

Object transforms — Partial / Required / Readonly #

Partial<T> — make every property optional #

Partial
type User = { id: string; name: string; age: number };
type UserPatch = Partial<User>;
// = { id?: string; name?: string; age?: number }

It’s frequently used as the input type of an update function.

patch function
function updateUser(id: string, patch: Partial<User>): void {
  // find the user by id and overwrite only the fields in patch
}

updateUser('u-1', { name: '영희' });        // ✓
updateUser('u-1', { name: '영희', age: 28 }); // ✓

Required<T> — make every optional required #

The opposite of Partial. Properties marked with ? become required.

Required
type Config = { host?: string; port?: number; timeout?: number };
type CompleteConfig = Required<Config>;
// = { host: string; port: number; timeout: number }

Readonly<T> — make every property readonly #

Readonly
type ImmutableUser = Readonly<User>;

const u: ImmutableUser = { id: 'u-1', name: '철수', age: 30 };
u.name = '영희';   // 🚫

Key selection — Pick / Omit / Record #

Pick<T, K> — keep only some keys #

Pick
type User = { id: string; name: string; age: number; email: string };
type UserPreview = Pick<User, 'id' | 'name'>;
// = { id: string; name: string }

K is a union of T’s keys. The new type is built from only the picked properties.

Omit<T, K> — drop some keys #

Omit
type UserWithoutEmail = Omit<User, 'email'>;
// = { id: string; name: string; age: number }

type UserWithoutEmailAndAge = Omit<User, 'email' | 'age'>;
// = { id: string; name: string }

To drop multiple keys, pass them as a union. Pick and Omit are near mirror images. “Pick what you need” is Pick; “drop what you don’t” is Omit.

Record<K, V> — key-value mapping object #

Record
type Scores = Record<string, number>;
// = { [key: string]: number }

const scores: Scores = {
  math: 95,
  english: 87,
  science: 91,
};

You can also restrict it to a specific union of keys.

Record with literal keys
type Roles = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Roles, string[]>;

const perms: Permissions = {
  admin: ['read', 'write', 'delete'],
  editor: ['read', 'write'],
  viewer: ['read'],
};
// missing any of the three is a compile error

This pattern is powerful for safely expressing data mapped to enum-like values.

Union transforms — Exclude / Extract / NonNullable #

Exclude<T, U> — drop some types from a union #

Exclude
type AllColors = 'red' | 'green' | 'blue' | 'yellow';
type WarmColors = Exclude<AllColors, 'green' | 'blue'>;
// = 'red' | 'yellow'

Drops the types in U from the union T.

Extract<T, U> — keep only some types from a union #

Extract
type Mixed = string | number | boolean | null | undefined;
type Truthy = Extract<Mixed, string | number | boolean>;
// = string | number | boolean

Mirror of Exclude.

NonNullable<T> — drop null/undefined #

NonNullable
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>;
// = User

A utility you’ll use often once you start dealing with T | null | undefined regularly.

Function-related — ReturnType / Parameters / Awaited #

ReturnType<F> — a function’s return type #

ReturnType
function fetchUser() {
  return { id: 'u-1', name: '철수' };
}

type User = ReturnType<typeof fetchUser>;
// = { id: string; name: string }

This is a pattern we briefly saw in #6 — you pull the shape of an object a function returns automatically, without defining a separate type.

Parameters<F> — a function’s parameter types (as a tuple) #

Parameters
function login(username: string, password: string): boolean {
  return true;
}

type LoginArgs = Parameters<typeof login>;
// = [username: string, password: string]

Awaited<T> — unwrap a Promise #

Awaited
type UserPromise = Promise<User>;
type UnwrappedUser = Awaited<UserPromise>;
// = User

// nested Promises also unwrapped
type Nested = Promise<Promise<User>>;
type Unwrapped = Awaited<Nested>;
// = User (unwrapped through nesting)

Frequently used for handling the return type of async functions.

Combined example — a realistic pattern #

A small example combining the utilities we’ve seen.

Realistic patterns
type User = {
  id: string;
  name: string;
  email: string;
  password: string;
  createdAt: Date;
};

// Exposed to clients — drop password
type PublicUser = Omit<User, 'password'>;

// Data from a signup form — id and createdAt are decided by the server
type SignupInput = Omit<User, 'id' | 'createdAt'>;

// Profile update — all optional except id
type ProfileUpdate = Partial<Omit<User, 'id'>>;

// User list card — only some fields needed
type UserCard = Pick<User, 'id' | 'name'>;

// Permission mapping
type Role = 'admin' | 'editor' | 'viewer';
type RolePermissions = Record<Role, string[]>;

// Pull parameters out of an API function signature
async function getUser(id: string): Promise<PublicUser> {
  // ...
  return {} as PublicUser;
}

type GetUserArgs = Parameters<typeof getUser>;     // [id: string]
type GetUserResult = Awaited<ReturnType<typeof getUser>>;  // PublicUser

From the same User data shape, you derive every variant you need per situation. Define the data shape in one place and derive views from it. The data-type sync burden disappears, and changes to User automatically propagate to derived types.

tsconfig.json — compiler settings #

Now to the second topic. tsconfig.json is the configuration file that controls every behavior of the TypeScript compiler.

The default config created by npx tsc --init comes with detailed comments. Open it once and you’ll see over 100 options. Fortunately, only a dozen or so matter day to day.

The key option — strict #

tsconfig.json (excerpt)
{
  "compilerOptions": {
    "strict": true
  }
}

strict: true is the master switch that turns on several safety options at once.

  • noImplicitAny — bans implicit any when inference fails
  • strictNullChecks — clearly distinguishes null and undefined from other types
  • strictFunctionTypes — strict function-parameter type compatibility
  • strictBindCallApply — checks the arguments to bind/call/apply
  • strictPropertyInitialization — enforces class property initialization
  • noImplicitThis — bans implicit-any this

For new projects, always start with strict: true. Turning it off cuts more than half the value of TypeScript. For migrating existing JavaScript projects, you may turn it on gradually, but turning strict off in a new project is almost always a wrong choice.

target — which JavaScript to compile to #

{ "compilerOptions": { "target": "ES2022" } }

Decides which version of JavaScript TypeScript compiles to.

  • ES5 — when you must support old IE (rare now)
  • ES2017ES2022 — modern browsers
  • ESNext — latest

For browser targets, usually ES2020 or above; for the latest Node.js, around ES2022 is reasonable.

module — module system #

{ "compilerOptions": { "module": "ESNext" } }

Decides how import/export code is compiled.

  • CommonJS — Node.js’s traditional require
  • ESNext / ES2022 — standard ES modules
  • NodeNext — Node.js’s mix of ES modules + CommonJS

If you use a bundler (Vite, Webpack), almost always ESNext. For pure Node.js projects, NodeNext is usually the answer.

moduleResolution — how to resolve modules #

{ "compilerOptions": { "moduleResolution": "bundler" } }

How TypeScript resolves import paths. Recently bundler (a new mode for bundlers like Vite/Webpack) or NodeNext is recommended.

lib — available built-in APIs #

{ "compilerOptions": { "lib": ["ES2022", "DOM", "DOM.Iterable"] } }

The built-in library types TypeScript should know about. If you run in browsers, include DOM; for Node-only, leave it out and use @types/node.

outDir / rootDir — input/output paths #

A simple setup
{
  "compilerOptions": {
    "rootDir": "./src",
    "outDir": "./dist"
  }
}
  • rootDir — the source root (where to start compiling)
  • outDir — where output files go

If you specify outDir, the compiled .js files collect there. Without it, they’re created next to the .ts files and clutter the folder.

include / exclude — which files to compile #

Specifying targets
{
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

include lists what to compile, exclude lists folders to skip. node_modules is almost always in exclude.

sourceMap — source maps for debugging #

{ "compilerOptions": { "sourceMap": true } }

Generates extra .js.map files so browser/Node debuggers can map back to your original .ts files. Debugging gets much easier.

jsx — for React projects #

React project
{ "compilerOptions": { "jsx": "react-jsx" } }

Decides how JSX is compiled. For modern React, react-jsx; for Next.js, preserve (Next.js handles it itself).

Putting it together — a modern recommended config #

For a new project, the following is a solid starting point.

tsconfig.json (modern recommended)
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "lib": ["ES2022", "DOM", "DOM.Iterable"],
    "jsx": "react-jsx",

    "strict": true,
    "noUncheckedIndexedAccess": true,

    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,

    "outDir": "./dist",
    "rootDir": "./src",
    "sourceMap": true,
    "declaration": true,
    "noEmit": false
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

Other options worth knowing:

  • noUncheckedIndexedAccess — adds | undefined to array index access. arr[0] becomes T | undefined instead of T. Big safety win, sometimes annoying. Recommend turning on if you can take it.
  • esModuleInterop — improves CommonJS/ES module interop. Almost always true.
  • skipLibCheck — skips checking external library .d.ts files. Faster compiles. Almost always true.
  • declaration — also generates .d.ts files. Needed when publishing a library.
  • noEmit — if true, doesn’t emit compile output. Used for type-checking only (when another tool like Vite does the transpile).

A Vite project doesn’t have tsc transpile — only type-check — so it’ll likely have noEmit: true.

Looking back at the series #

In this series we covered:

#TopicCore
1Getting started and setupTS motivation, compile flow, first code
2Basic typesstring/number/boolean/array/tuple/object/enum, any/unknown
3interface and type aliasnaming object types, the differences between the two tools
4union/literal/narrowingexpressing multiple possibilities + type narrowing
5Function typesoptional/default/rest, overloads, intro to generics
6Generics in depthconstraints, keyof, indexed access, conditional types
7Utility types + tsconfigstandard utilities, compile setup

Learning TypeScript isn’t about absorbing everything in one pass — it’s an area where familiarity grows as you write code. With this series you’ve met the names and shapes of nearly every tool, and you can now think “ah, I need that narrowing pattern from #4” when you get stuck while writing code.

Next steps #

Tackle these right away #

  • Migrate one of your small JavaScript projects to TS — rename .js to .ts and fix errors one at a time
  • Start new projects in TS from scratch — pick the TS option in Vite or Next.js
  • Read a library’s .d.ts file — opening node_modules/lodash/index.d.ts shows real-world types in practice

What you’ll need soon #

  • @types/... packages — adds types to libraries that don’t ship them
  • Zod or Valibot — runtime validation libraries. Combine with TS for higher safety.
  • TypeScript ESLint — lint rules (e.g., no-explicit-any, prefer-const)

Follow-up series — React + TypeScript #

The just-finished React series of 31 posts is all in JavaScript. Next, the React + TypeScript series covers how to express all those patterns inside TS — props typing, hook typing, generic components, polymorphism. The patterns you use most in real work.

Wrap-up #

Thank you for following along through 7 posts. TypeScript has a steep initial curve, but cross a certain threshold and it becomes a tool you can’t go back from. The value of autocomplete, refactoring safety, and immediate feedback is hard to forget once experienced.

If there’s one thing to remember — it’s the habit of declaring your data shapes. Just the habit of writing function signatures, object types, and API response shapes explicitly at the top of your code makes you write better code. Types are ultimately a tool to make you think more deeply about your code.

Go back to a small project you’d like to build, and this time start with TypeScript. Real learning happens where you get stuck.

X