Skip to content

Instantly share code, notes, and snippets.

@angelikatyborska
Last active January 30, 2023 10:14
Show Gist options
  • Save angelikatyborska/cea5696595be44b930498bfd5e124454 to your computer and use it in GitHub Desktop.
Save angelikatyborska/cea5696595be44b930498bfd5e124454 to your computer and use it in GitHub Desktop.
TypeScript notes
// If I define a union type of strings and want to use it as a type for object keys:
type Food = 'Banana' | 'Berries'
const calorieCounts: { [index in Food]: number } = {} // <- NOT `index: Food`
// ----------
// If I define an object and want to use its keys as a type:
// NOTE the object shouldn't have a type definition for this to work!
const planetYearsToEarthYears = { earth: 1, mercury: 0.24 }
type Planet = keyof typeof planetYearsToEarthYears
// ----------
// Official TS docs recommend to use Interface over Type
// https://www.typescriptlang.org/play?&e=83#code/PTAEBUAsFMCdtAQ3qALgdwPagLaIJYB2ammANgM4mgAm0AxmcgqjKBZIgA4KYBmSQgFgAUCFCYARgCsGqAFygiqOH0T1oVRIRpoAnjyRl8iCpoB0okFbBRoepCgBucBxXw58TWABpBuvkxYNDYcTApUUHpMHDDielNNGyR6SNYECkQcaEsRUVQDBAAhfFgacELQAF5QAG9RUFB0IgBzCkUAJgBuUQBfHryRZVV1YtKaAElCFVg1DTqGptb20G6+0VFowgjQSXGARkUSsorDGtqlwjbO0H7NzG3IvbKOo-GpmbmEc8vr1duBskigxEABXMwQQoAZXosHwXEi+C07FQsFBqVBsEQZH0hgoegi0BwPmS+FQAHIqFxwu5JGQWNhhrBPAAPELQUqgcEMzC5e6PXbjADMbzKHxG8xqzxo+0BYlsMAckkwrHYoK41NgkWgLJUOlaEnSwSZXy0OlxFmSpwQ2JMZioNGwrCRoCcJiU0zgZlS+AeFoofnQkC80FJntmoyonBcSFAAGt7FgynyRAVDAB5dA4n6ETAY2CEbGKVGghC9UAAMlAx3KhQGaYQACUpERqnVQLn84WyIo1JQy5Xq+9w185SbRqAAArQdR5uOgHV6mhUGvWhYiRrRMiYTF8UE9tBo6ADRp8YyaXvYswDXqicfzADCwfoCeIi+gOhXw8+E-qG6i5A7rM+6Xv2J6gGe+AXhBV7Husgz0pEmBZoombZu2zRXCsHR+J2qCYt2xZHgCoiIVEz6vooT74C+H5thcmF-DhAHbruIEwf2fiQdBfYQncgziFAiCRJk+A0IGCDwNEsQfroeg7lyEL3poEguMEDbJLaiQUOYoBQjw9D4HwNHYmQeh+JIILcqA8mgksZA4i00BPM5MwLrAsBBMk2QUJkTk6aAEwCLZoCQJgamqXA7IQeQ26Me5nmwAGNkKQkwjyuw0AIGF6CQjwMJwgiUTaKAXCeW6dBoF6UXaLoYQoIE9DcjQ3maH5KlBnRSZxgazSqspVDGAmoDUbRhApsh6H0BRH4DNNNGvm2k1yuI6aEAgeDSEEtBGXwcAfvMlkYFlxANkYdoqU4VADZpKCsMJHo-hoWgoJgPDELVFrndpjgIIw4TQDQuSCcGVDZNoVAhWlC66rJgiPRKCCSA4dCMMgBpkppmVbLoqAeDkGxDCOE4ANJknq66NFwmJJYoyrkDOhA3oTymgGTqAU3+m6AZiigRHCVzMwJYBTNFKowMECQQogX1pbmLnkdoTm6DuqDuJV-CkmrtAMN4wm+uNhNnZO6pcA45yLFuQR86irQ3nKxum+blMkASiiEKCOCWbA9uE+IAAi0DvTQBp+rZwQtJgV5+M6Dq7fthDzNE+66JZSDJNS7h4zG20y+tLT6y4ukABLIdAEWBME1N0jRZkw5ngPJA2yVkpSsZHW5CQOdQeDDekODw8pKaretEgCOkuyaJE8AUEBz0xcEZgclcRg4vw0WA05yRSypyA7uazeutdxPPTHoMouo87haocVN5A8CILoLoy5HmC6FwTDzKg2ARMgCh+2ASAHMuDtBAL-F819ZhxXMNJYAABHUsEQDYUGAEKAA7B0IU6ChQAFZgDN1hPCVAABaAaxCrrEObsAHBHQABsAAODoTCAAMABiGhDCmEdGYaIIAA
// ----------
// > The `as const` suffix acts like const but for the type system,
// > ensuring that all properties are assigned the literal type instead of a more general version like string or number.
const req = { url: "https://example.com", method: "GET" } as const;
// ----------
// `in` is for checking keys in objects, not for arrays!
> 'x' in {x: 31, y: 23}
true
> 'x' in ['x', 'y']
false
> ['x', 'y'].includes('x')
true
// ----------
// how to overload type definitions for one function
// (e.g. if given argument of type X, returns type A, if given argument of type Y, returns type B)
function plusOne(x: string): string;
function plusOne(x: number): number;
function plusOne(x: string | number) { // <- DO NOT define return type here
if (typeof x === 'number') {
return x + 1
} else {
return `${x} plus one`
}
}
// (correct TS, but might break some eslint rules like no-redeclare and @typescript-eslint/explicit-function-return-type :<)
// ----------
// `never` appears when TypeScript determines there’s nothing left in a union.
function fn(x: string | number) {
if (typeof x === "string") {
// do something
} else if (typeof x === "number") {
// do something else
} else {
x; // has type 'never'!
}
}
// ----------
// object properties can be marked as readonly, but
// TypeScript doesn’t factor in whether properties on two types are readonly
// when checking whether those types are compatible
interface SomeType {
readonly prop: string;
}
// ----------
// you can define an interface with both known and unknown keys of known types
interface NumberOrStringDictionary {
[index: string]: number | string;
length: number; // ok, length is a number
name: string; // ok, name is a string
}
// ----------
// tuples are arrays of known length
type StringNumberPair = [string, number];
type Either2dOr3d = [number, number, number?];
// ----------
// hints for using React with TS:
// https://react-typescript-cheatsheet.netlify.app/docs/basic/getting-started/basic_type_example
// ----------
// Types for React refs:
// - `MutableRefObject<T>` if you yourself assign to `ref.current`
// - `RefObject<T>` if you let React handle assignment
//
// In both cases, initial value of null as allowed even if T doesn't cover null
// ----------
@jiegillet
Copy link

For plusOne, a good alternative is to use generics:

function plusOne<T>(x: T) {
  if (typeof x === 'number') {
    return x + 1
  } else {
    return `${x} plus one`
  }
}

@angelikatyborska
Copy link
Author

angelikatyborska commented Jan 23, 2023

@jiegillet No, that doesn't achieve the same thing. With your example, calling const a = plusOne('hi') results in the type of a being string | number, but the goal is to make it only a string.

@angelikatyborska
Copy link
Author

I attempted a lot of things yesterday to make it happen, based on tutorials I saw, but nothing with generics worked. For example, this is wrong even though https://www.youtube.com/watch?v=lMfGp29Ht8c&list=PLIvujZeVDLMx040-j1W4WFs1BxuTGdI_b recommends this:

function plusOne<T extends number | string>(x: T): T extends number ? number : string {
  if (typeof x === 'number') {
    return x + 1
  } else {
    return `${x} plus one`
  }
}

That gives me errors, including:

TS2322: Type 'number' is not assignable to type 'T extends number ? number : string'.

But my understanding of narrowing in TS is that it should work...

@jiegillet
Copy link

Right, I just got to the same result too, there must be something...

@jiegillet
Copy link

Ok, this works but... meh..

type GetReturnType<T> = T extends number ? number : string

function plusOne<T>(x: T): GetReturnType<T> {
  if (typeof x === 'number') {
    return x + 1 as GetReturnType<T>
  } else {
    return `${x} plus one` as GetReturnType<T>
  }
}

@angelikatyborska
Copy link
Author

YES! that works

@angelikatyborska
Copy link
Author

...but it's also not correct 😵‍💫 if I accidentally mess up the number branch to return a string, TypeScript won't notice, it will still think it returned a number.

@jiegillet
Copy link

Yeah, I think it's a consequence of using as, it basically mean "shut up, I know better than you, stupid machine"

@mtarnovan
Copy link

...but it's also not correct 😵‍💫 if I accidentally mess up the number branch to return a string, TypeScript won't notice, it will still think it returned a number.

My knowledge of Typescript is very limited, but I don't think what you want can ever work with Typescript: the type erasure is done at "compile"/build time, not at runtime. Typescript cannot evaluate the if clause at build time to know that one branch needs to return a number while the other one a string; it can only check that the function returns a type that satisfies the declared return type.

// in is for checking keys in objects, not for arrays!

That's the plain old Javascript in operator though, not Typescript, right?

@angelikatyborska
Copy link
Author

@mtarnovan

My knowledge of Typescript is very limited, but I don't think what you want can ever work with Typescript: the type erasure is done at "compile"/build time, not at runtime. Typescript cannot evaluate the if clause at build time to know that one branch needs to return a number while the other one a string; it can only check that the function returns a type that satisfies the declared return type.

I have been talking here about compile time checks only, no runtime stuff. It can work. TypeScript does a lot of static code analysis to narrow down types. See this modified example, with reloaded function signatures, where I make one of the return values have a type that doesn't fulfill the signature:

function plusOne(x: string): string;
function plusOne(x: number): number;
function plusOne(x: string | number) {
  if (typeof x === 'number') {
    return x + 1
  } else {
    return false
  }
}

It produces this error:

file.tsx:83:10 - error TS2394: This overload signature is not compatible with its implementation signature.

83 function plusOne(x: string): string;
            ~~~~~~~

  file.tsx:85:10
    85 function plusOne(x: string | number) {
                ~~~~~~~
    The implementation signature is declared here.

That's the plain old Javascript in operator though, not Typescript, right?

Yes! But I literally never used in in JavaScript, and as it started appearing in TypeScript for type definitions and type guards, I got confused 😵

@mtarnovan
Copy link

You're totally right, I wasn't expecting Typescript to be so smart, but it is, i.e. it correctly infers the return type in each of the branches. I wonder why for the second branch it doesn't reduce the return type to string 🤔

Screenshot 2023-01-24 at 11 02 34

Screenshot 2023-01-24 at 11 04 38

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment