Previously, I wrote a markdown-related component that inserted the string generated by the back-end parsing of markdown into the DOM via dangerouslySetInnerHTML, and set a number of styles that were introduced via modular CSS.

1
2
3
4
5
6
7
8
import style from "./MarkdownBody.module.css"

const MarkdownBody: react.FC<Props> = ({ content }) => {

    return (
        <div className={style.markdown} dangerouslySetInnerHTML={{ __html: content }}></div>
    )
}

It had been working fine before, but recently I had another page that needed to be styled the same as the markdown page, and of course the most intuitive way to do that is to just bring up the style file, but an interesting question comes to mind here: is it also not possible for the props of a component, or the parameters of a function, to be type-safe and defined as mutually exclusive?

1
2
3
4
5
<MarkdownBody content={...} />

<MarkdownBody>
    <div>...</div>
</MarkdownBody>

The same component, without children, passing only a props named content, is rendered differently than the case where no content is passed, but a child component.

Or, as an example of a function.

1
2
3
foo(p1); // 允许
foo(p2); // 允许
foo(p1, p2); // 错误

Of course, even without TypeScript, JS itself allows functions to be called with a mismatch to their definition.

1
2
3
4
5
6
function foo(a, b) {
    console.log(a, b);
}

foo(); // undefined undefined
foo(1); // 1 undefined

You can tell if a parameter is passed by determining if it is undefined, and in TypeScript you can define both parameters as optional. But if this code is in a third-party library, you can only expect the user to read the documentation and follow the conventions. Is there a type-safe way to do this?

Union Types

Union types Union Types can be defined in TypeScript, e.g.

1
let foo: string | number;

But it is useless to define the parameters directly as the union type of two interfaces.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface P1 {
    a: string
}

interface P2 {
    b: number
}

function foo(p: P1 | P2) {
    console.log(p)
}

// 都可以通过编译
foo({a: "hello"})
foo({b: 123})
foo({a: "hello", b: 12})

never

Some languages have a type system where there is a bottom type that is a subtype of all types. In TypeScript, there is such a type, never, which can be used to represent the return value (or lack thereof) of a function that only throws exceptions or internal dead loops. It has the property that no value of any other type can be assigned to a variable of this type.

1
2
3
4
5
6
let foo: never = 123; // Error: number 类型不能赋值给 never 类型

// ok, 作为函数返回类型的 never
let bar: never = (() => {
  throw new Error('Throw my hands in the air like I just dont care');
})();

Using this property, the above code is modified to.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type Param = {
    a: string
    b?: never
} | {
    a?: never
    b: number
}

function foo(p: Param) {
    console.log(p)
}

foo({a: "hello"})
foo({b: 123})
foo({a: "hello", b: 12}) // 报错

ts

This ensures that the user will only use one of the two mutually exclusive properties, and a simple conditional rendering within the component will do the trick.