Skip to content

armanozak/typescript-questions

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 

Repository files navigation

TypeScript Questions

Back in April and May 2023, I asked daily TypeScript questions on my Twitter account for 30 working days. This repository is originally a compilation of these questions, but if you want to contribute, please feel free to create a PR.

Q1

const foo = "1234";

What is the type of foo?

Answer

"1234"

TypeScript infers this type as a string literal and not as string because it is a constant. If we had used let, then the inferred type would have been string.

Can we force TypeScript to use string here? Yes.

const foo: string = "1234"

Info: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types

Q2

enum Flag {
  Foo,
  Bar = -2,
  Baz
}

What are the values of Foo and Baz flags?

Note: This is just an example to test your knowledge. I recommend assigning all enum values explicitly, even when they are ordered.

Answer

Foo = 0, Baz = -1

When an enum member doesn't have an initializer, it is assigned as previous member value +1 or 0 if it is the first member.

Info: https://www.typescriptlang.org/docs/handbook/enums.html#computed-and-constant-members

Q3

const myObject = {
  id: crypto.randomUUID(),
};

type Id =                        ;
//                  ^
// What should I write here to create a type alias
// for the id property of myObject?

I want to create a type alias for myObject's id property. How should I fill in the blank to do this?

Answer

typeof myObject.i​d

We want to keep the Id type alias compatible with the actual type of that property, so we can't;

  • use hard-coded types.
  • leverage any technique that gets the return type of randomUUID (because it can change too).

Info: https://www.typescriptlang.org/docs/handbook/2/typeof-types.html

Q4

function add(x: number, y: number) {
  return x + y;
}

const params = [1, 2];

add(...params);
//      ^
// I'm getting an error here. How can I make it go away?

I want to spread those parameters, but I'm getting an error. What should I do?

Answer
const params: Parameters<typeof add> = [1, 2];

Can't we use a tuple instead? Yes, assigning [number, number] to or using as const on params will work too. However, deriving the type from the parameters of the add function will always keep them in sync.

Info: https://www.typescriptlang.org/docs/handbook/utility-types.html#parameterstype

Q5

interface Foo {
  x: number;
  y: number;
}

interface Bar {
  q: string;
}

let fooOrBar: Foo | Bar;

fooOrBar = { q: "" };
type T1 = typeof fooOrBar;

fooOrBar = { x: 0, y: 1 };
type T2 = typeof fooOrBar;

What are T1 and T2?

Answer

T1 = Bar, T2 = Foo

The type of fooOrBar is a union type (Foo | Bar). However, TypeScript is able to narrow types based on assignments. So, T1 becomes Bar. Then we assign another value and TypeScript narrows the type again, this time as Foo.

Info: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#assignments

Q6

class SplitAfterEach {
  constructor(public readonly search: string) {}

  [Symbol.split](str: string) {
    const result: string[] = [];

    do {
      let index = str.indexOf(this.search);
      if (index < 0) {
        result.push(str);
        break;
      }

      index += this.search.length;
      result.push(str.substring(0, index));
      str = str.substring(index);
    } while (str.length);

    return result;
  }
}

console.log("foobarfoobar".split(new SplitAfterEach("foo")));

type StringKeysOfSplitAfterEach =                              ;
//                                             ^
// How can I get only the string keys of SplitAfterEach?

How should I fill in the blank to get only the string keys of SplitAfterEach?

Answer

Option 1: keyof SplitAfterEach & string

We can get keys with keyof, but keys can be of number or symbol type too. An intersection type narrows it for us.

Info: https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types

Option 2: Use a utility type

Extract<keyof SplitAfterEach, string>

Info: https://www.typescriptlang.org/docs/handbook/utility-types.html#extracttype-union

Q7

type X = Awaited<Promise<Promise<string>>>;

What is X?

Answer

string

Awaited is a utility type that unwraps promises recursively.

Info: https://www.typescriptlang.org/docs/handbook/utility-types.html#awaitedtype

Q8

type BuildEvent = "BuildError" | "BuildSuccess";
type HttpEvent = "HttpError" | "HttpSuccess";
type InitEvent = "InitError" | "InitSuccess";
type NavEvent = "NavError" | "NavSuccess";

type AppEvent = BuildEvent | HttpEvent | InitEvent | NavEvent;

type AppError = Extract<AppEvent,                   >;
//                                        ^
// What should I write here to get all error types?
// i.e. "BuildError" | "HttpError" | "InitError" | "NavError"

How should I fill in the blank to get all error types and none of the success types?

Answer

`${string}Error`

Template literals allow us to compose string literal types, and string acts like a wildcard. So, when Extract asserts the assignability of the AppEvent union against ${string}Error, all members ending with "Error" pass.

Info: https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

Q9

type BuildEvent = "BuildError" | "BuildSuccess";
type HttpEvent = "HttpError" | "HttpSuccess";
type InitEvent = "InitError" | "InitSuccess";
type NavEvent = "NavError" | "NavSuccess";

type AppEvent = BuildEvent | HttpEvent | InitEvent | NavEvent;

// This works. The type is a union of all errors.
type AppError = Extract<AppEvent, `${string}Error`>;

// This doesn't! The type is never.
type AppNever = AppEvent extends `${string}Error` ? AppEvent : never;

// Why?

In the last question, we used Extract to get all error types. Extract is a very simple generic type:

type Extract<T, U> = T extends U ? T : never;

However, when I implement the same conditional type directly, it doesn't work. Why?

Answer

Generic types distribute unions.

Every member of a union is evaluated separately when passed to any generic type. So this works too:

type ExtractError<T> = T extends `${string}Error` ? T : never;
type AppError = ExtractError<AppEvent>;

Info: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#distributive-conditional-types

Q10

// human.ts
export class Human {
  constructor(public readonly name: string) {}

  breath() {
    console.log(`${this.name} breaths`);
  }

  eat(food: string) {
    console.log(`${this.name} eats ${food}`);
  }

  sleep() {
    console.log(`${this.name} sleeps`);
  }

  walk() {
    console.log(`${this.name} walks`);
  }
}
// speech.ts
import { Human } from "./human";

Human.prototype.speak = function (this: Human, sth: string) {
  //              ^
  // I'm getting an error: Property 'speak' does not exist on type 'Human'.

  console.log(`${this.name} says "${sth}"`);
};


// How can I add the "speak" method the "Human" interface?
// index.ts
import { Human } from "./human";
import "./speech";

const human = new Human("Guru");

human.speak("Hello world!");

How can I add a speak method to the Human interface using the speech module?

Answer

With module augmentation.

Placing the code below in speech.ts will add the speak method to the Human interface:

declare module './human' {
  interface Human {
    speak(str: string): void;
  }
}

Info: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation

Q11

/* Note: This file is imported by the entry file. */

import * as MSW from "msw";

(global as any).msw = MSW;

// How can I add "msw" to the global scope in TypeScript?

I want to use the MSW library in my test files without importing it in each file, so I mutated the global object. How can I add msw to the global scope in TypeScript too?

Answer

With global augmentation.

Adding this to the test-globals.ts file will declare msw as a global variable:

declare global {
  const msw: typeof MSW;
}

Info: https://www.typescriptlang.org/docs/handbook/declaration-merging.html#global-augmentation

Q12

type MapLink = string;

interface Address {
  street: string;
  no: string;
  postalCode: string;
  city: string;
  country: string;
}

interface Coordinates {
  x: number;
  y: number;
}

declare function getLinkForAddress(address: Address): MapLink;
declare function getLinkForCoordinates(coordinates: Coordinates): MapLink;

function getLink(addressOrCoordinates: Address | Coordinates) {
  if (isCoordinates(addressOrCoordinates)) {
    //      ^
    // This doesn't narrow the type to Coordinates and I get errors.
    return getLinkForCoordinates(addressOrCoordinates);
  }

  return getLinkForAddress(addressOrCoordinates);
}

// What should I add to isCoordinates function to narrow the type?
function isCoordinates(input: object) {
  return "x" in input && "y" in input;
}

I am getting errors when I pass addressOrCoordinates as an argument to getLinkForCoordinates and getLinkForAddress functions. What should I add to isCoordinates function to narrow the type?

Answer

input is Coordinates as return type

function isCoordinates(
  input: object
): input is Coordinates {
  return "x" in input && "y" in input;
}

This kind of return type is called a "type predicate".

Info: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates

Q13

interface Petrol {
  engineType: "combustion";
  fillTank(): void;
}

interface Diesel {
  engineType: "combustion";
  fillTank(): void;
}

interface Electric {
  engineType: "electricity";
  charge(): void;
}

interface Hybrid {
  engineType: "combustion & electricity";
  fillTank(): void;
  charge(): void;
}

type CarType = Petrol | Diesel | Electric | Hybrid;

function getArea(car: CarType) {
  switch (car.engineType) {
    case "combustion":
      return car.fillTank();
    case "electricity":
      return car.charge();
    default:
      return exhaustCases(car);
      //                   ^
      // How can I declare this function so that I get a type error here?
  }
}

I want to get a type error when not all cases are exhausted. How should I declare the exhaustCases function for that? Please use the declare function ... syntax.

Answer
declare function exhaustCases(
  input: never
): never;

never is assignable to all types, but no other type is assignable to never. If the case clauses in your switch statement exhaust all members of the union type, TypeScript will narrow the type in the default clause down to never, so there will be no errors. In this example, the Hybrid interface is not exhausted, so we would get an error if we try to assign it to a parameter with never type.

Info: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#exhaustiveness-checking

Why is the return type never and not void? 🤔

Well, in this example, it is possible to return void. However, we would have to update it if we ever update the return types of fillTank and charge methods, otherwise we would end up with a union type for no reason. Take a look at this playground: https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgAoTFA9gG2QbwChlkIQBzUCAFQE8AHCALmQCIEsBbAIwFcBnMMCwhWAbmLIYwHDmpwQAawAUAShbcsuCAokBfQoVCRYiFABFgEfhDxESZSiBoNmbDjwFCR4ydNnySmoaWjg6IPqGxtDwSMgAomEImMAIBJKOVHSMLKy2EMlQqcBgtL4kCAAWcFDkEMHImtq6hAZG4DFmyAAStNxFACbpDhRZrrkefILCIMgAZKRJKQglZRIk-nIKKuqNoeHryFU1dQ1NYS1tpYzIAMI12SgAvGgY2HgAPsiW1rbIX4kCst-j0+oMJIQYLwQMkZsgoBAYAj+JV4s5arQAMpYXhQJDKBA1Fj3KCPVTIAD0ACo9s1Zl8AG5YYBDKkU4bIfgAdxKVWQBJqADpMs4yRyKnAbO4uFNvKImJISCQEWBcbNCVBBZtAjtDhKpXklkUVqVWAqlUqVWqjkLjrV6qo9cgBoi4LwcGBzRb4RhrRAAB7VLz3Gz8AVQR2SAxtF0IHA1FBQmFy0iBt2CEPWZSgei8T3IZwM6C7JkssRAA

never as return type is the best option because it won't change the return type of the function and will always work. So, we can even have a common exhaustCases function that works for all switch statements.

Q14

type Operation = "upsert" | "delete";

interface Options {
  skipErrors?: boolean;
}

// None of these should lead to type errors:
execute("upsert", "foo");
execute("delete", "foo", { skipErrors: false });
execute("upsert", "foo", "bar");
execute("delete", "foo", "bar", { skipErrors: true });
execute("upsert", "foo", "bar", "baz");
execute("delete", "foo", "bar", "baz", {});
execute("upsert", ...(["foo", "bar"] as const));
execute("delete", ...(["foo", "bar"] as const), {});
execute("upsert", "x", ...["foo", "bar", "baz"]);
execute("delete", "x", ...["foo", "bar", "baz"], {});

// All of these should lead to type errors:
execute("upsert");
execute("delete", {});
execute("upsert", {}, "foo");
execute("delete", 1, "foo");
execute("upsert", "foo", 1, "bar");
execute("delete", "foo", { skipErrors: "true" });
execute("foo", "bar", "baz");

// How should I declare the execute function?
// Note: Use void as return type.

How should I declare the execute function? You can use void as its return type. Please use the declare function ... syntax.

Answer
declare function execute(
  operation: Operation,
  requiredParam: string,
  ...optionalParams: string[] | [...string[], Options]
): void;

Using an overload here is worse than a union type because of two reasons:

  • Instead of just "skipErrors", TypeScript auto-complete displays all string methods with an "s".
  • execute("upsert", "foo", 1, "bar") is completely highlighted as an error because it doesn't match any overloads, whereas with union types only the part that doesn't match the signature is highlighted.

Please try not to declare a function like that in your work. 😅

Q15

type A = void extends true ? 1 : 0;
type B = true extends void ? 1 : 0;
type X = (() => void) extends (() => true) ? 1 : 0;
type Y = (() => true) extends (() => void) ? 1 : 0;

What are A, B, X, and Y?

Answer

A = 0, B = 0, X = 0, Y = 1

void and true don't extend each other, but void as a return type has a special status to allow this:

declare const records: string[];
declare function register(rec: string): boolean;
records.forEach(register);

Info: https://www.typescriptlang.org/docs/handbook/2/functions.html#return-type-void

Q16

class Point {
  get #self() {
    return this as Mutable<Point>;
  }

  constructor(
    public readonly x: number,
    public readonly y: number
  ) {}

  move(x: number, y: number) {
    this.#self.x += x;
    this.#self.y += y;
  }

  [Symbol.iterator]() {
    return [this.x, this.y][Symbol.iterator]();
  }
}

const point = new Point(0, 1);
console.log(...point); // 0 1

point.move(2, 3);
console.log(...point); // 2 4

Please declare the Mutable type and make sure it is reusable.

Answer
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

Removing modifiers like readonly and optional (?) is possible while mapping types.

Info: https://www.typescriptlang.org/docs/handbook/2/mapped-types.html#mapping-modifiers

Q17

interface Point {
  readonly x: number;
  readonly y: number;
}

type Mutable<T, Keys extends keyof T = keyof T> = Reveal<
  {
    -readonly [K in keyof Pick<T, Keys>]: T[K];
  } & Omit<T, Keys>
>;

type HorizontallyMovablePoint = Mutable<Point, "x">;
// {
//   x: number;
//   readonly y: number;
// }

Let's add the option to pick keys to the Mutable type in the previous question. What should the Reveal type be, if we want to see the whole interface and not some obscure intersection type on hover?

Answer
type Reveal<T> = {
  [K in keyof T]: T[K];
} & {};

Q18

interface Point {
  [stringIndex: string]: any;
  [symbolIndex: symbol]: any;
  [templateLiteralIndex: `distanceTo${string}`]: number;
  name: string;
  readonly x: number;
  readonly y: number;
  move(x: number, y: number): void;
}

type BasePoint = OmitIndexSignatures<Point>;
// type BasePoint = {
//   name: string;
//   readonly x: number;
//   readonly y: number;
//   move: (x: number, y: number) => void;
// }

How should I declare the OmitIndexSignatures type so that BasePoint won't have any index signatures?

Ref: https://www.typescriptlang.org/docs/handbook/2/objects.html#index-signatures

Answer
type OmitIndexSignatures<T> = {
  [
    K in keyof T
      as {} extends Record<K, 1> ? never : K
  ]: T[K];
};

Credit: https://stackoverflow.com/questions/51465182/how-to-remove-index-signature-using-mapped-types/68261113#68261113

Q19

type T0 = Await<Promise<boolean>>; // boolean
type T1 = Await<Promise<Promise<number>>>; // number
type T2 = Await<Promise<Promise<Promise<symbol>>>>; // symbol
type T3 = Await<Pick<Promise<string>, 'then'>>; // string
type T4 = Await<{then<T>(mapFn: (url: URL) => T, flag: boolean): T}>; // URL
type T5 = Await<{then(): Promise<URL>}>; // never
type T6 = Await<{then: Event}>; // { then: Event }
type T7 = Await<Date>; // Date
type T8 = Await<null>; // null
type T9 = Await<undefined>; // undefined

Please declare the Await type without using (or peeking at 🙂) the built-in Awaited type. You may assume strict mode.

Answer
type Await<T> =
  T extends {
    then: (fn: infer Fn, ...args: any[]) => any
  }
    ? Fn extends (value: infer V, ...args: any[]) => any
        ? Await<V>
        : never
    : T;

Conditional types allow us to infer types with the infer keyword.

Info: https://www.typescriptlang.org/docs/handbook/2/conditional-types.html#inferring-within-conditional-types

Q20

interface Component {
  on_mount(): void;
  on_update(): void;
  on_destroy(): void;
  parent_ref?: Component;
}

type HookName = ParseHookNames<Component>;
//      ^
// "Mount" | "Update" | "Destroy"

Please declare the ParseHookNames type.

Answer
type ParseHookName<T> =
  T extends `on_${infer K}`
    ? Capitalize<K>
    : never;

type ParseHookNames<T> = ParseHookName<keyof T>;

Info: https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html#inference-with-template-literals

Q21

abstract class Child {
  play() {};
}

abstract class Adult {
  doWhatYouWant() {};
  haveFun() {};
  spendTimeWithLovedOnes() {};
  rest() {};
}

class Parent extends Adult {
  constructor(public children: [Child, ...Child[]]) {
    super();
  }
}

class WorkingAdult extends Working(Adult) {}

class WorkingParent extends Working(Parent) {
  haveSomeTimeOff() {
    this.children.forEach(child => child.play());
    return super.haveSomeTimeOff();
  }
}

function spendToday(workers: WorkingAdult[]): boolean {
  return workers.every(person => person.haveSomeTimeOff());
}

Please declare the Working function.

Answer
type AbstractConstructor<T> = abstract new (...args: any[]) => T;
type AbstractClass<T> = AbstractConstructor<T> & { prototype: T };

interface WorkingImpl {
  work(): void; // just for presentational purposes
  haveSomeTimeOff(): boolean;
}

declare function Working<T extends AbstractConstructor<Adult>>(
  Base: T
): AbstractClass<WorkingImpl> & T;

Info: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-2.html#abstract-construct-signatures

Q22

declare const uniqueSymbol: unique symbol;
interface Foo { n: number; }
interface Bar { n: number; }
interface Baz { n: number; a: boolean; }

type T01 = IsSame<null, null>; // true
type T02 = IsSame<undefined, undefined>; // true
type T03 = IsSame<void, void>; // true
type T04 = IsSame<null, undefined>; // false
type T05 = IsSame<null, void>; // false
type T06 = IsSame<void, null>; // false
type T07 = IsSame<undefined, void>; // false
type T08 = IsSame<void, undefined>; // false
type T09 = IsSame<true, boolean>; // false
type T10 = IsSame<number, number>; // true
type T11 = IsSame<number, 0>; // false
type T12 = IsSame<1, number>; // false
type T13 = IsSame<symbol, symbol>; // true
type T14 = IsSame<symbol, typeof uniqueSymbol>; // false
type T15 = IsSame<string, string>; // true
type T17 = IsSame<string, ''>; // false
type T18 = IsSame<'X', string>; // false
type T19 = IsSame<string, Promise<string>>; // false
type T20 = IsSame<Promise<string>, Promise<string>>; // true
type T21 = IsSame<Date, Date>; // true
type T22 = IsSame<object, Date>; // false
type T23 = IsSame<object, object>; // true
type T24 = IsSame<{}, object>; // false
type T25 = IsSame<{}, {}>; // true
type T26 = IsSame<Foo, {}>; // false
type T27 = IsSame<Foo, Bar>; // true
type T28 = IsSame<Foo, {n: number}>; // true
type T29 = IsSame<Foo, Baz>; // false
type T30 = IsSame<Baz, Foo>; // false

Please declare the IsSame type. We should be able to switch the two generic variable types and get the same result.

Answer
type IsSame<T1, T2> =
  (<T>() => (T extends T1 ? 1 : 0)) extends
  (<T>() => (T extends T2 ? 1 : 0))
    ? true
    : false;

Credit: microsoft/TypeScript#27024 (comment)

How does it work? Here's a comment from the TypeScript codebase:

Two conditional types 'T1 extends U1 ? X1 : Y1' and 'T2 extends U2 ? X2 : Y2' are related if one of T1 and T2 is related to the other, U1 and U2 are identical types, X1 is related to X2, and Y1 is related to Y2.

Let's rewrite our type to observe the resemblance:

type IsSame<U1, U2> =
  (<T1>() => (T1 extends U1 ? 'X' : 'Y')) extends
  (<T2>() => (T2 extends U2 ? 'X' : 'Y'))
    ? true
    : false;

To resolve the outer conditional type, TypeScript checks if U1 and U2 are identical, and that's exactly what we need. 🤷‍♂️

Q23

interface Point {
  x: number;
  y: number;
  name?: string;
}

interface Circle {
  center: Point;
  radius: number;
  name?: string;
}

type RequiredKeysOfPoint = RequiredKeysOf<Point>;
// "x" | "y"

type RequiredKeysOfCircle = RequiredKeysOf<Circle>;
// "center" | "radius"

Please declare the "RequiredKeysOf" type and make sure it is reusable.

Answer
type RequiredKeysOf<T> = RequiredKeysOf1<OmitIndexSignatures<T>>;

type RequiredKeysOf1<T> = {
  [K in keyof T]-?: {} extends Pick<T, K> ? never : K;
} [keyof T];

type RequiredKeysOf2<T> = {
  [K in keyof T]-?: T extends Record<K, T[K]> ? K : never;
}[keyof T];

type RequiredKeysOf3<T> = keyof {
  [K in keyof T as {} extends Pick<T, K> ? never : K]: unknown;
};

type RequiredKeysOf4<T> = keyof {
[K in keyof T as T extends Record<K, T[K]> ? K : never]: unknown;
};

type OmitIndexSignatures<T> = {
  [K in keyof T as {} extends Record<K, 1> ? never : K]: T[K];
}

All 4 options work as long as index signatures are removed. Options 1 & 2 reveal the keys even when all keys are required, whereas options 3 & 4 return keyof T (e.g. keyof Point).

Q24

interface Bank { code: string; }
interface Currency { code: string; sign: string; }

type OpaqueBank = Opaque<Bank, "BANK">;
type OpaqueCurrency = Opaque<Currency, "CURRENCY">;
type OpaqueIBAN = Opaque<string, "IBAN">;

interface BankAccount {
  bank: OpaqueBank;
  currency: OpaqueCurrency;
  holder: string;
  iban: OpaqueIBAN;
}

declare const account: BankAccount;

// Assigning a value directly to "bank", "currency" or "iban" should be an error.
account.bank = { code: "TCZBTR2AXXX" }; // Error
account.bank = account.currency; // Error
account.currency = { code: "USD", sign: "$" }; // Error
account.iban = "TR320010009999901234567890"; // Error

// But, it should be possible to use them as their base type.
declare function formatCurrency(amount: number, currency: Currency): string;
const formattedAmount = formatCurrency(128_000_000_000, account.currency);

declare function formatIBAN(iban: string): string;
const formattedIBAN = formatIBAN(account.iban);

// This should be "bank" | "currency" | "iban".
type OpaqueKeysOfBankAccount = OpaqueKeysOf<BankAccount>;

// This should be "BANK" | "CURRENCY" | "IBAN".
type OpaqueTokensOfBankAccount = OpaqueTokensOf<BankAccount>;

// And, these should be allowed.
(account.bank as Transparent<OpaqueBank>) = { code: "TCZBTR2AXXX" };
(account.currency as Transparent<OpaqueCurrency>) = { code: "USD", sign: "$" };
(account.iban as Transparent<OpaqueIBAN>) = "TR320010009999901234567890";
(account as ClearOpaqueKeysOf<BankAccount, "bank">).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount, "bank" | "iban">).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount, string>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount, unknown>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount, any>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount, "currency">).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueKeysOf<BankAccount, "currency" | "iban">).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueKeysOf<BankAccount>).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueKeysOf<BankAccount, "iban">).iban = "TR320010009999901234567890";
(account as ClearOpaqueKeysOf<BankAccount, "bank" | "iban">).iban = "TR320010009999901234567890";
(account as ClearOpaqueKeysOf<BankAccount>).iban = "TR320010009999901234567890";
(account as ClearOpaqueTokensOf<BankAccount, "BANK">).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount, "BANK" | "IBAN">).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount, string>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount, unknown>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount, any>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount>).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount, "CURRENCY">).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueTokensOf<BankAccount, "CURRENCY" | "IBAN">).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueTokensOf<BankAccount>).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueTokensOf<BankAccount, "IBAN">).iban = "TR320010009999901234567890";
(account as ClearOpaqueTokensOf<BankAccount, "BANK" | "IBAN">).iban = "TR320010009999901234567890";
(account as ClearOpaqueTokensOf<BankAccount>).iban = "TR320010009999901234567890";

// But, these should not be allowed.
(account as ClearOpaqueKeysOf<BankAccount, "iban">).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueKeysOf<BankAccount, "bank">).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueKeysOf<BankAccount, "currency">).iban = "TR320010009999901234567890";
(account as ClearOpaqueTokensOf<BankAccount, "IBAN">).bank = { code: "TCZBTR2AXXX" };
(account as ClearOpaqueTokensOf<BankAccount, "BANK">).currency = { code: "USD", sign: "$" };
(account as ClearOpaqueTokensOf<BankAccount, "CURRENCY">).iban = "TR320010009999901234567890";

Please define the Opaque, Transparent, OpaqueKeysOf, OpaqueTokensOf, ClearOpaqueKeysOf, and ClearOpaqueTokensOf types.

Answer
declare const brand: unique symbol;

type Branded<Token extends string> = {
  readonly [brand]: Token;
};

type Opaque<T, Token extends string> = T & Branded<Token>;

type Transparent<T extends Branded<any>> =
  T extends Opaque<infer U, T[typeof brand]>
    ? U
    : T;

type OpaqueKeysOf<T> = {
  [P in keyof T]: T[P] extends Branded<any> ? P : never;
}[keyof T];

type OpaqueTokensOf<T> = {
  [P in keyof T]: T[P] extends Branded<infer U> ? U : never;
}[keyof T];

type ClearOpaqueKeysOf<T, K = OpaqueKeysOf<T>> = {
  [P in keyof T]: P extends K
    ? T[P] extends Branded<any>
      ? Transparent<T[P]>
      : T[P]
    : T[P];
};

type ClearOpaqueTokensOf<T, K = OpaqueTokensOf<T>> = {
  [P in keyof T]: T[P] extends Branded<infer Token>
    ? Token extends K
      ? Transparent<T[P]>
      : T[P]
    : T[P];
};

This is an approach to nominal typing in TypeScript, as described in this official example: https://www.typescriptlang.org/play#example/nominal-typing

Q25

interface App {
  run(): void;
}

type T1 = TupleOf<boolean, 4>; // [boolean, boolean, boolean, boolean];
type T2 = TupleOf<number, 5>;  // [number, number, number, number, number];
type T3 = TupleOf<Date, 6>;    // [Date, Date, Date, Date, Date, Date];
type T4 = TupleOf<string, 99>; // [string, ... 97 more ..., string];
type T5 = TupleOf<App, 999>;   // [App, ... 997 more ..., App];

Please declare the TupleOf type. You may hit a limit depending on your TypeScript version (use v4.5+) and implementation.

Answer
type TupleOf<
  T,
  N extends number,
  Acc extends T[] = []
> = N extends Acc['length']
  ? Acc
  : TupleOf<T, N, [...Acc, T]>;

We don't have to worry about negative numbers, decimals, or Infinity as input because they will hit the limit and TypeScript will give an error.

Q26

type HelloWorld1 = Concat<["Hello", " ", "world"]>;
// "Hello world"

type HelloWorld2 = Concat<["H", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]>;
// "Hello world"

type SingleLetter = Concat<["X"]>;
// "X"

type EmptyString = Concat<[]>;
// ""

type NonString = Concat<["a", "b", 3]>;
//                            ^
// Error: Type 'number' is not assignable to type 'string';

Please declare the Concat type.

Answer
type Concat<T extends string[]> =
  T extends [
    infer Head extends string,
    ...infer Tail extends string[]
  ]
    ? `${Head}${Concat<Tail>}`
    : "";

When the iteration is complete and Head becomes undefined, the outer condition fails because undefined doesn't extend string.

Q27

type T0 = Split<"">;
// []

type T1 = Split<"X">;
// ["X"]

type T2 = Split<"Hello world">;
// ["H", "e", "l", "l", "o", " ", "w", "o", "r", "l", "d"]

type T3 = Split<"CR#7">;
// ["C", "R", "#", "7"]

Please declare the Split type.

Answer
type Split<T extends string> =
  T extends `${infer Head}${infer Tail}`
    ? [Head, ...Split<Tail>]
    : [];

Adding a delimiter parameter to both Split and Concat (from #Q26) is possible:

type Split<T extends string, Delimiter extends string = ""> =
  "" extends T
    ? []   
    : T extends `${infer Head}${Delimiter}${infer Tail}`
      ? [Head, ...Split<Tail, Delimiter>]
      : [T];

type Concat<T extends string[], Delimiter extends string = ""> = 
  T extends [infer Head extends string, ...infer Tail extends string[]]
    ? `${Head}${Delimiter}${Concat<Tail>}`
    : "";


type S0 = Split<"">;  // []
type C0 = Concat<S0>; // ""

type S1 = Split<"", "X">;  // []
type C1 = Concat<S1, "X">; // ""

type S2 = Split<"X">; // ["X"]
type C2 = Concat<S2>; // "X"

type S3 = Split<"X", "X">; // [""]
type C3 = Concat<S3, "X">; // "X"

type S4 = Split<"Hello world", " ">; // ["Hello", "world"]
type C4 = Concat<S4, " ">;           // "Hello world"

type S5 = Split<"CR#7">; // ["C", "R", "#", "7"]
type C5 = Concat<S5>;    // "CR#7"

type S6 = Split<"CR#7", "#">; // ["CR", "7"]
type C6 = Concat<S6, "#">;    // "CR#7"

Q28

type T0 = Reverse<[]>;               // []
type T1 = Reverse<[boolean]>;        // [boolean]
type T2 = Reverse<[null, void]>;     // [void, null]
type T3 = Reverse<["x", "y", "z"]>;  // ["z", "y", "x"]
type T4 = Reverse<[0, 1, 2, 3, 4]>;  // [4, 3, 2, 1, 0]
type T5 = Reverse<[{}, object]>;     // [object, {}]

Please declare the Reverse type.

Answer
type Reverse<T extends unknown[]> =
  T extends [infer Head, ...infer Tail]
    ? [...Reverse<Tail>, Head]
    : [];

Bonus (string variant):

type ReverseStr<T extends string> =
  T extends `${
    infer Head extends string
  }${
    infer Tail extends string
  }`
    ? `${ReverseStr<Tail>}${Head}`
    : '';

type T0 = ReverseStr<''>; // ""
type T1 = ReverseStr<'X'>; // "X"
type T2 = ReverseStr<'hello'>; // "olleh"

Q29

class Bar extends Foo {}
class Baz extends Foo {
  constructor(public readonly x: number, public readonly y: number) {
    super();
  }
}
class Qux extends Baz {}

const foo = Foo.create();
//           ^
// Error: Cannot assign an abstract constructor type to a non-abstract constructor type.

const bar = Bar.create();
// OK. Type of bar is Bar.

const baz = Baz.create();
//                 ^
// Error: Expected 2 arguments, but got 0.

const qux = Qux.create(0, 1);
// OK. Type of qux is Qux.

Please declare the abstract Foo class and implement the static create method. It doesn't have to do anything other than returning an instance.

Answer
abstract class Foo {
  static create<
    T extends Foo,
    Args extends unknown[]
  >(this: new (...args: Args) => T, ...args: Args) {
    return new this(...args);
  }
}

Info: https://www.typescriptlang.org/docs/handbook/2/classes.html#static-members

Q30

type T01 = IsGTE<0, 0>; // true
type T02 = IsGTE<0, 1>; // false
type T03 = IsGTE<1, 0>; // true
type T04 = IsGTE<1, 1>; // true
type T05 = IsGTE<42, 24>; // true
type T06 = IsGTE<42, 99>; // false
type T07 = IsGTE<999, 1000>; // false
type T08 = IsGTE<1000, 999>; // true
type T09 = IsGTE<1000, 1000>; // true
type T10 = IsGTE<-0, -0>; // true
type T11 = IsGTE<-0, -1>; // true
type T12 = IsGTE<-1, -0>; // false
type T13 = IsGTE<-1, -1>; // true
type T14 = IsGTE<-42, -24>; // false
type T15 = IsGTE<-42, -99>; // true
type T16 = IsGTE<-999, -1000>; // true
type T17 = IsGTE<-1000, -999>; // false
type T18 = IsGTE<-1000, -1000>; // true
type T19 = IsGTE<0, -0>; // true
type T20 = IsGTE<0, -1>; // true
type T21 = IsGTE<1, -1>; // true
type T22 = IsGTE<42, -24>; // true
type T23 = IsGTE<42, -42>; // true
type T24 = IsGTE<1000, -1000>; // true
type T25 = IsGTE<-0, 0>; // true
type T26 = IsGTE<-1, 0>; // false
type T27 = IsGTE<-1, 1>; // false
type T28 = IsGTE<-42, 0>; // false
type T29 = IsGTE<-42, 42>; // false
type T30 = IsGTE<-1000, 1000>; // false

Please declare the IsGTE type. Generic parameters will always be numeric literals.

Answer
type And<T extends (() => boolean)[], I extends 0[] = []> = 
  I['length'] extends T['length']
    ? true
    : ReturnType<T[I['length']]> extends false
      ? false
      : And<T, [...I, 0]>;

type Or<T extends (() => boolean)[], I extends 0[] = []> =
  I['length'] extends T['length']
    ? false
    : ReturnType<T[I['length']]> extends true
      ? true
      : Or<T, [...I, 0]>;

type Not<T extends boolean> = T extends true ? false : true;

type IsEqual<N1, N2> =
  (<T>() => (T extends N1 ? 1 : 0)) extends
  (<T>() => (T extends N2 ? 1 : 0))
    ? true
    : false;

type IsNegative<T extends number> = `${T}` extends `-${number}` ? true : false;

type IsSmallerNegative<
  A extends string,
  B extends string,
  I extends 0[] = []
>
  = `-${I['length']}` extends infer M extends A | B
    ? M extends A ? false : true
    : IsSmallerNegative<A, B, [...I, 0]>;

type IsGTE<
  A extends number,
  B extends number
> =
  Or<[
    () => IsEqual<A, B>,

    () => And<[
      () => IsNegative<B>,
      () => Not<IsNegative<A>>
    ]>,

    () => And<[
      () => IsNegative<A>,
      () => IsNegative<B>,
      () => IsSmallerNegative<`${B}`, `${A}`>
    ]>,
    
    () => And<[
      () => Not<IsNegative<A>>,
      () => Not<IsNegative<B>>,
      () => IsSmallerNegative<`-${A}`, `-${B}`>
    ]>,
  ]>;

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published