the_previ
the_previ

Reputation: 681

Define class in Typescript module

I'm writing a Typescript module for an external library.

Here's a piece of my index.d.ts

declare module 'my-external-library' {
  export class MyComponent extends React.Component<MyComponentProps> {}

  export class CustomClass {
    constructor(
     firstParam: string,
     secondParam: string,
    )
  }

  interface MyComponentProps {
    config: string
    customClass: CustomClass
    text?: string
    customFn?: (input: string) => boolean
  }

}

This works fine except for the customClass prop. Custom Class is a class that has to be like this:

class customClass {
  constructor(firstParam, secondParam) {
    this.firstParam = firstParam
    this.secondParam = secondParam
  }

  ... all the methods you want
}

export default customClass

In a separate file I give the customClass props to MyComponent, like this:


import customClass from './customClass'

<MyComponent
    config: "test"
    customClass: customClass
/>

customClass actually is not receiving the "constructor types" declared in index.d.ts, but all other props of the component works well. So, the class definition I've made is correct? I have to export the class with another approach?

Here's a CodeSandbox: https://codesandbox.io/s/react-typescript-playground-forked-nrfk2

Thanks for any help, forgive my inexperience

Upvotes: 3

Views: 1568

Answers (1)

hackape
hackape

Reputation: 20007

I've forked and edited your code sandbox, check it out:

Edit React Typescript Playground (forked)

Now I'm gonna explain a few things about class in typescript, which is indeed quite confusing.

It's in part due to the fact that people use the term "class" to refer to diff things in diff contexts. To disambiguate, let's use more accurate terms.

Constructor

MyPuppy is often called a "class", but it's also a "constructor function" in JS terms. So let's stick to constructor throughout this discussion.

// javascript
class MyPuppy {}

Instance

Variable puppy is an instance of MyPuppet

// javascript
const puppy = new MyPuppet()

Instance Type vs Constructor Type

In typescript, when you declare a class like below, you'll implicitly declare an instance type of the same name!

This behaviour confuses a lot of people. In short, MyPuppy as a JS variable holds a constructor, whereas MyPuppy as a TS type variable actually holds an instance type.

Now how do we refer to the type of MyPuppy constructor, aka the constructor type? You should literally use typeof MyPuppy.

// typescript
class MyPuppet {}

type MyPuppetInstance = MyPuppet
type MyPuppetConstructor = typeof MyPuppet

We've done disambiguating terms, let's do some case study.

There's this concept of the "instance side" and "static side" of a class, once documented in the old TS handbook. Basically "instance side" corresponds to instance type, "static side" to constructor type.

class MyPuppy {
  static getSpecies(dog: MyPuppy) {
    return dog.species
  }

  constructor(species: string) { this.species = species }

  species: string

  // [side note] above two lines can be combined as one:
  // constructor(public species: string) {}

  poop() { return '💩' }
}

type MyPuppetInstance = MyPuppet
type MyPuppetConstructor = typeof MyPuppet


var bar: MyPuppetConstructor = MyPuppy
var foo: MyPuppetInstance = new bar("shiba_inu")

The above snippet does several things.

  1. Declare a JS constructor of the name MyPuppy.
  2. Implicitly declare a TS instance type MyPuppy.
  3. On the "instance side", foo is an instance, and has a property species and a method poop.
  4. On the "static side", the bar is a constructor, it's a "newable function" which can be invoked as new bar("shiba_inu"), and also has a static method getSpecies.

Now in a .d.ts file, how can we declare the above types? You can use either the interface keyword or the type keyword. They make almost no difference.

// index.d.ts

interface MyPuppyInstance {
  species: string
  poop(): string
}

interface MyPuppyConstructor {
  new (species: string): MyPuppyInstance
  getSpecies(dog: { species: string }): string
}

type MyPuppyInstance2 = {
  species: string
  poop(): string
}

type MyPuppyConstructor2 = 
  (new (species: string) => MyPuppyInstance)
  & { getSpecies(dog: { species: string }): string }

Upvotes: 4

Related Questions